Kopiér og indsæt dette eksempelscript, og rediger det efter behov for dit miljø:

< nr. . SYNOPSIS     Samler JSON-data om sikker bootstart fra flere enheder i oversigtsrapporter.

. BESKRIVELSE     Læser indsamlede JSON-filer med status for sikker bootstart og genererer:     - HTML-dashboard med diagrammer og filtrering     - Oversigt efter ConfidenceLevel     - Entydig enhedsspandanalyse til teststrategi          Understøtter:     - Filer pr. computer: HOSTNAME_latest.json (anbefales)     - Enkelt JSON-fil          Deduplikerer automatisk af HostName og beholder den seneste CollectionTime.     Som standard omfatter kun enheder med "Handlingssikkerhed" eller "Høj"     for at fokusere på buckets, der kan handles på. Brug -IncludeAllConfidenceLevels til at tilsidesætte.

. PARAMETER InputPath     Sti til JSON-fil(er):     - Mappe: Læser alle *_latest.json-filer (eller *.json, hvis der ikke er nogen _latest filer)     - Fil: Læser en enkelt JSON-fil

. PARAMETER OutputPath     Sti til genererede rapporter (standard: .\SecureBootReports)

. EKSEMPEL     # Sammenlæg fra mappe med filer pr. computer (anbefales)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$"     # Læser: \\contoso\SecureBootLogs$\*_latest.json

. EKSEMPEL     # Brugerdefineret outputplacering     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -OutputPath "C:\Reports\SecureBoot"

. EKSEMPEL     # Medtag kun Handlingsanmodning og Høj konfidens (standardfunktion)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$"     # Udelader: Observation, afbrudt, understøttes ikke

. EKSEMPEL     # Medtag alle konfidensniveauer (tilsidesæt filter)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeAllConfidenceLevels

. EKSEMPEL     # Filter for brugerdefineret konfidensniveau     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncludeConfidenceLevels @("Action Req", "High", "Observation")

. EKSEMPEL     # ENTERPRISE SCALE: Trinvis tilstand – kun behandle ændrede filer (hurtigt efterfølgende kører)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode     # Første kørsel: Fuld indlæsning ~2 timer for 500.000 enheder     # Efterfølgende kører: Sekunder, hvis ingen ændringer, minutter for deltaer

. EKSEMPEL     # Spring HTML over, hvis intet er ændret (hurtigst til overvågning)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -IncrementalMode -SkipReportIfUnchanged     # Hvis ingen filer er blevet ændret siden sidste kørsel: ~5 sekunder

. EKSEMPEL     # Kun oversigtstilstand – spring store enhedstabeller over (1-2 minutter vs. 20+ minutter)     .\Aggregate-SecureBootData.ps1 -InputPath "\\contoso\SecureBootLogs$" -SummaryOnly     # Genererer CSV-filer, men springer HTML-dashboardet over med komplette enhedstabeller

. NOTER     Parre med Detect-SecureBootCertUpdateStatus.ps1 til virksomhedsinstallation.Se GPO-DEPLOYMENT-GUIDE.md for at få en komplet installationsvejledning.     Standardfunktionsmåden udelukker observation, midlertidigt afbrudte og ikke-understøttede enheder     for kun at fokusere på rapportering på enhedsspand, der kan handles ud fra.#>

param(     [Parameter(Obligatorisk = $true)]     [streng]$InputPath,          [Parameter(Obligatorisk = $false)]     [string]$OutputPath = ".\SecureBootReports",          [Parameter(Obligatorisk = $false)]     [string]$ScanHistoryPath = ".\SecureBootReports\ScanHistory.json",          [Parameter(Obligatorisk = $false)]     [streng]$RolloutStatePath, # Sti til RolloutState.json til at identificere InProgress-enheder          [Parameter(Obligatorisk = $false)]     [streng]$RolloutSummaryPath, # Sti til SecureBootRolloutSummary.json fra Orchestrator (indeholder projektionsdata)          [Parameter(Obligatorisk = $false)]     [streng[]]$IncludeConfidenceLevels = @("Handling påkrævet", "Høj konfidens"), # Medtag kun disse konfidensniveauer (standard: kun handlingsbaserede buckets)          [Parameter(Obligatorisk = $false)]     [switch]$IncludeAllConfidenceLevels, # Tilsidesæt filter for at medtage alle konfidensniveauer          [Parameter(Obligatorisk = $false)]     [switch]$SkipHistoryTracking,          [Parameter(Obligatorisk = $false)]     [switch]$IncrementalMode, # Aktivér deltabehandling – indlæs kun ændrede filer siden sidste kørsel          [Parameter(Obligatorisk = $false)]     [streng]$CachePath, # Sti til cachemappe (standard: OutputPath\.cache)          [Parameter(Obligatorisk = $false)]     [int]$ParallelThreads = 8, # Antal parallelle tråde til filindlæsning (PS7+)          [Parameter(Obligatorisk = $false)]     [switch]$ForceFullRefresh, # Gennemtving fuld genindlæsning selv i trinvis tilstand          [Parameter(Obligatorisk = $false)]     [switch]$SkipReportIfUnchanged, #Skip HTML/CSV generation if no files changed (just output stats)          [Parameter(Obligatorisk = $false)]     [switch]$SummaryOnly, # Generér kun oversigtsstatistik (ingen store enhedstabeller) – meget hurtigere          [Parameter(Obligatorisk = $false)]     [switch]$StreamingMode # Hukommelseseffektiv tilstand: processegmenter, skriv CSV-filer trinvist, bevar kun opsummeringer i hukommelsen )

# Opløft automatisk til PowerShell 7, hvis det er tilgængeligt (6x hurtigere for store datasæt) if ($PSVersionTable.PSVersion.Major -lt 7) {     $pwshPath = Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -ExpandProperty-kilde     if ($pwshPath) {         Write-Host "PowerShell $($PSVersionTable.PSVersion) registreret – start igen med PowerShell 7 for hurtigere behandling..." -Forgrundsfarve gul         # Gendan argumentliste ud fra bundne parametre         $relaunchArgs = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $MyInvocation.MyCommand.Path)         foreach ($key i $PSBoundParameters.Keys) {             $val = $PSBoundParameters[$key]             if ($val -is [switch]) {                 hvis ($val. IsPresent) { $relaunchArgs += "-$key" }             } elseif ($val -is [array]) {                 $relaunchArgs += "-$key"                 $relaunchArgs += ($val -joinforbindelse ',')             } ellers {                 $relaunchArgs += "-$key"                 $relaunchArgs += "$val"             }         }         & $pwshPath @relaunchArgs         afslut $LASTEXITCODE     } }

$ErrorActionPreference = "Fortsæt" $timestamp = Get-Date -Format "yyyyMMdd-HHmmss" $scanTime = Get-Date -Format "åååå-MM-dd HH:mm:ss" $DownloadUrl = "https://aka.ms/getsecureboot" $DownloadSubPage = "Deployment and Monitoring Samples"

# Bemærk! Dette script har ingen afhængigheder af andre scripts. # Du kan få det komplette værktøjssæt ved at downloade fra: $DownloadUrl -> $DownloadSubPage

#region installation Write-Host "=" * 60 -Forgrundsfarve cyan Write-Host "Secure Boot Data Aggregation" -ForegroundColor Cyan Write-Host "=" * 60 -Forgrundsfarve cyan

# Opret outputmappe if (-not (Test-Path $OutputPath)) {     New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null }

# Indlæs data – understøtter CSV-formater (ældre) og JSON-formater (oprindelige) Write-Host "'nIndlæs data fra: $InputPath" -Forgrundsfarve gul

# Helper-funktion til at normalisere enhedsobjekt (håndtag feltnavn forskelle) funktionen Normalize-DeviceRecord {     param($device)          # Handle Hostname vs HostName (JSON bruger Hostname, CSV bruger HostName)     hvis ($device. PSObject.Properties['Hostname'] -and -not $device. PSObject.Properties['HostName']) {         $device | Add-Member -NotePropertyName 'HostName' -NotePropertyValue $device. Hostname -Force     }          # Handle Confidence vs ConfidenceLevel (JSON bruger Konfidens, CSV bruger ConfidenceLevel)     # ConfidenceLevel er det officielle feltnavn - kort Konfidens til det     hvis ($device. PSObject.Properties['Confidence'] -and -not $device. PSObject.Properties['ConfidenceLevel']) {         $device | Add-Member -NotePropertyName 'ConfidenceLevel' -NotePropertyValue $device. Konfidens – gennemtving     }          # Spor opdateringsstatus via Event1808Count OR UEFICA2023Status="Updated"     # Dette gør det muligt at spore, hvor mange enheder i hver konfidensbøtte der er blevet opdateret     $event 1808 = 0     hvis ($device. PSObject.Properties['Event1808Count']) {         $event 1808 = [int]$device. Hændelse1808Tæl     }     $uefiCaUpdated = $false     hvis ($device. PSObject.Properties['UEFICA2023Status'] -and $device. UEFICA2023Status -eq "Opdateret") {         $uefiCaUpdated = $true     }          hvis ($event 1808 -gt 0 -eller $uefiCaUpdated) {         # Markér som opdateret for dashboard-/udrulningslogik – men oversæt ikke ConfidenceLevel         $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $true -Force     } ellers {         $device | Add-Member -NotePropertyName 'IsUpdated' -NotePropertyValue $false -Force                  # Konfidensniveauklassifikation:         # - "Høj konfidens", "Under observation...", "Midlertidigt afbrudt...", "Understøttes ikke..." = brug som den er         # - Alt andet (null, tom, "UpdateType:...", "Ukendt", "I/T") = falder til Handling Påkrævet i tællere         # Ingen normalisering er nødvendig – streamingtællerens anden gren håndterer den     }          # Handle OEMManufacturerName vs WMI_Manufacturer (JSON bruger OEM*, ældre bruger WMI_*)     hvis ($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     hvis ($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     hvis ($device. PSObject.Properties['FirmwareVersion'] -and -not $device. PSObject.Properties['BIOSDescription']) {         $device | Add-Member -NotePropertyName 'BIOSDescription' -NotePropertyValue $device. FirmwareVersion – Gennemtving     }          returner $device }

#region trinvis behandling/cachestyring # Konfigurer cachestier if (-not $CachePath) {     $CachePath = Join-Path $OutputPath ".cache" } $manifestPath = Join-Path $CachePath "FileManifest.json" $deviceCachePath = Join-Path $CachePath "DeviceCache.json"

# Cachestyringsfunktioner funktionen Get-FileManifest {     param([streng]$Path)     if ($Path teststi) {         prøv {             $json = Get-Content $Path -Raw | ConvertFrom-Json             # Konvertér PSObject til hashtable (PS5.1-kompatibel – PS7 har -AsHashtable)             $ht = @{}             $json. PSObject.Properties | ForEach-Object { $ht[$_. Navn] = $_. Værdi }             returnere $ht         } fange {             returner @{}         }     }     returner @{} }

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

funktionen Get-DeviceCache {     param([streng]$Path)     if (teststi $Path) {         prøv {             $cacheData = Get-Content $Path -Raw | ConvertFrom-Json             Write-Host " Indlæst enhedscache: $($cacheData.Count) enheder" -ForegroundColor DarkGray             returner $cacheData         } fange {             Write-Host " Cache beskadiget, genopbygges" -Forgrundsfarve gul             returner @()         }     }     returner @() }

funktionen Save-DeviceCache {     param($Devices; [streng]$Path)     $dir = Split-Path $Path -Parent     if (-not (Test-Path $dir)) {         New-Item -ItemType Directory -Path $dir -Force | Out-Null     }     # Konvertér til matrix, og gem     $deviceArray = @($Devices)     $deviceArray | ConvertTo-Json -Depth 10 -Compress | Set-Content $Path -Force     Write-Host " Gemt enhedscache: $($deviceArray.Count) enheder" -Forgrundsfarve DarkGray }

funktionen Get-ChangedFiles {     param(         [System.IO.FileInfo[]]$AllFiles,         [hashtable]$Manifest     )          $changed = [System.Collections.ArrayList]::new()     $unchanged = [System.Collections.ArrayList]::new()     $newManifest = @{}          # Opbyg opslag, hvor der ikke skelnes mellem store og små bogstaver, fra manifest (normaliser til små bogstaver)     $manifestLookup = @{}     foreach ($mk i $Manifest.Keys) {         $manifestLookup[$mk. ToLowerInvariant()] = $Manifest[$mk]     }          foreach ($file i $AllFiles) {         $key = $file. FullName.ToLowerInvariant() # Normaliser sti til små bogstaver         $lwt = $file. LastWriteTimeUtc.ToString("o")         $newManifest[$key] = @{             LastWriteTimeUtc = $lwt             Størrelse = $file. Længde         }                  if ($manifestLookup.ContainsKey($key)) {             $cached = $manifestLookup[$key]             hvis ($cached. LastWriteTimeUtc -eq $lwt -og $cached. Størrelse - $file. Længde) {                 [void]$unchanged. Add($file)                 Fortsætte             }         }         [void]$changed. Add($file)     }          returner @{         Ændret = $changed         Uændret = $unchanged         NewManifest = $newManifest     } }

# Ultra-hurtig parallel filindlæsning ved hjælp af batched processing funktionen Load-FilesParallel {     param(         [System.IO.FileInfo[]]$Files,         [int]$Threads = 8     )

$totalFiles = $Files. Tælle     # Brug batches af ~1000 filer hver for bedre hukommelseskontrol     $batchSize = [matematik]::Min(1000, [matematik]::Loft($totalFiles / [matematik]::Maks(1, $Threads)))     $batches = [System.Collections.Generic.List[object]]:new()     

for ($i = 0; $i -lt $totalFiles; $i += $batchSize) {         $end = [matematik]::Min($i + $batchSize, $totalFiles)         $batch = $Files[$i.. ($end-1)]         $batches. Add($batch)     }     Write-Host " ($($batches. Antal) batches af ~$batchSize filer hver)" -NoNewline -ForegroundColor DarkGray     $flatResults = [System.Collections.Generic.List[object]]:new()     # Kontrollér, om PowerShell 7+ parallel er tilgængelig     $canParallel = $PSVersionTable.PSVersion.Major -ge 7     if ($canParallel -and $Threads -gt 1) {         # PS7+: Procesbatches parallelt         $results = $batches | ForEach-Object -ThrottleLimit $Threads -Parallel {             $batchFiles = $_             $batchResults = [System.Collections.Generic.List[object]]:new()             foreach ($file i $batchFiles) {                 prøv {                     $content = [System.IO.File]::ReadAllText($file. FullName) | ConvertFrom-Json                     $batchResults.Add($content)                 } fange { }             }             $batchResults.ToArray()         }         foreach ($batch i $results) {             if ($batch) { foreach ($item i $batch) { $flatResults.Add($item) } }         }     } ellers {         # PS5.1 fallback: Sekventiel behandling (stadig hurtig til <10K-filer)         foreach ($file i $Files) {             prøv {                 $content = [System.IO.File]::ReadAllText($file. FullName) | ConvertFrom-Json                 $flatResults.Add($content)             } fange { }         }     }     returner $flatResults.ToArray() } #endregion                         

$allDevices = @() if (Test-Path $InputPath -PathType Leaf) {     # Enkelt JSON-fil     if ($InputPath -like "*.json") {         $jsonContent = Get-Content -Path $InputPath -Raw | ConvertFrom-Json         $allDevices = @($jsonContent) | ForEach-Object { Normalize-DeviceRecord $_ }         Write-Host "Indlæste $($allDevices.Count)-poster fra fil"     } ellers {         Write-Error "Kun JSON-format understøttes. Filen skal have .json filtypenavn."         afslut 1     } } elseif (teststi $InputPath -PathType-objektbeholder) {     # Mappe - kun JSON     $jsonFiles = @(Get-ChildItem -Path $InputPath -Filter "*.json" -Recurse -ErrorAction SilentlyContinue |                    Where-Object { $_. Navn -notmatch "ScanHistory|RolloutState|RolloutPlan" })          # Foretrækker *_latest.json filer, hvis de findes (tilstand pr. computer)     $latestJson = $jsonFiles | Where-Object { $_. Name -like "*_latest.json" }     if ($latestJson.Count -gt 0) { $jsonFiles = $latestJson }          $totalFiles = $jsonFiles.Count          if ($totalFiles -eq 0) {         Write-Error "Ingen JSON-filer fundet i: $InputPath"         afslut 1     }          Write-Host "Fundet $totalFiles JSON-filer" -Forgrundsfarve grå          #Helper-funktion til at matche tillidsniveauer (håndterer både korte og fulde formularer)     # Defineret tidligt, så både StreamingMode og normale stier kan bruge den     funktionen Test-ConfidenceLevel {         param([streng]$Value, [streng]$Match)         if ([string]::IsNullOrEmpty($Value)) { return $false }         ($Match) {             "HighConfidence" { return $Value -eq "Høj konfidens" }             "UnderObservation" { return $Value -like "Under Observation*" }             "ActionRequired" { return ($Value -like "*Action Required*" -or $Value -eq "Action Required") }             "Midlertidig pause" { returnerer $Value -like "Midlertidigt afbrudt*" }             "NotSupported" { return ($Value -like "Not Supported*" -or $Value -eq "Not Supported") }             standard { return $false }         }     }          #region STREAMING-TILSTAND – Hukommelseseffektiv behandling af store datasæt     # Brug altid StreamingMode til hukommelseseffektiv behandling og nyt dashboard     if (-not $StreamingMode) {         Write-Host "Automatisk aktivering af StreamingMode (nyt dashboard)" -Forgrundsfarve gul         $StreamingMode = $true         if (-not $IncrementalMode) { $IncrementalMode = $true }     }          # Når -StreamingMode er aktiveret, skal du behandle filer i dele, der kun bevarer tællere i hukommelsen.# Data på enhedsniveau skrives til JSON-filer pr. segment til indlæsning efter behov i dashboardet.# Hukommelsesforbrug: ~1,5 GB uanset datasætstørrelse (vs. 10-20 GB uden streaming).if ($StreamingMode) {         Write-Host "STREAMING MODE enabled - memory-efficient processing" -ForegroundColor Green         $streamSw = [System.Diagnostics.Stopwatch]::StartNew()         # TRINVIS KONTROL: Hvis ingen filer er blevet ændret siden sidste kørsel, skal du helt springe behandlingen over         if ($IncrementalMode -and -not $ForceFullRefresh) {             $stManifestDir = Join-Path $OutputPath ".cache"             $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json"             if ($stManifestPath teststi) {                 Write-Host "Søger efter ændringer siden sidste streaming run..." -ForegroundColor Cyan                 $stOldManifest = Get-FileManifest -Path $stManifestPath                 if ($stOldManifest.Count -gt 0) {                     $stChanged = $false                     # Hurtig kontrol: samme antal filer?                     if ($stOldManifest.Count -eq $totalFiles) {                         # Kontrollér de 100 NYESTE filer (sorteret efter LastWriteTime faldende)                         # Hvis en fil ændres, har den det seneste tidsstempel og vises først                         $sampleSize = [matematik]::Min(100, $totalFiles)                         $sampleFiles = $jsonFiles | Sort-Object LastWriteTimeUtc -Faldende | Select-Object – første $sampleSize                         foreach ($sf i $sampleFiles) {                             $sfKey = $sf. FullName.ToLowerInvariant()                             if (-not $stOldManifest.ContainsKey($sfKey)) {                                 $stChanged = $true                                 Bryde                             }                             # Sammenlign tidsstempler – cachelagret kan være DateTime eller streng efter JSON-rundtur                             $cachedLWT = $stOldManifest[$sfKey]. LastWriteTimeUtc                             $fileDT = $sf. LastWriteTimeUtc                             prøv {                                 # Hvis cachelagret allerede er DateTime (ConvertFrom-Json auto-converts), skal du bruge direkte                                 if ($cachedLWT -is [DateTime]) {                                     $cachedDT = $cachedLWT.ToUniversalTime()                                 } ellers {                                     $cachedDT = [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime                                 }                                 if ([math]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) {                                     $stChanged = $true                                     Bryde                                 }                             } fange {                                 $stChanged = $true                                 Bryde                             }                         }                     } ellers {                         $stChanged = $true                     }                     if (-not $stChanged) {                         # Kontrollér, om outputfiler findes                         $stSummaryExists = Get-ChildItem (joinsti $OutputPath "SecureBoot_Summary_*.csv") -EA SilentlyContinue | Select-Object -Første 1                         $stDashExists = Get-ChildItem (joinsti $OutputPath "SecureBoot_Dashboard_*.html") -EA SilentlyContinue | Select-Object -Første 1                         if ($stSummaryExists -and $stDashExists) {                             Write-Host " Ingen ændringer fundet ($totalFiles filer uændret) - springer behandling over" -ForegroundColor Green                             Write-Host " Sidste dashboard: $($stDashExists.FullName)" -Forgrundsfarve hvid                             $cachedStats = Get-Content $stSummaryExists.FullName | ConvertFrom-Csv                             Write-Host " Enheder: $($cachedStats.TotalDevices) | Opdateret: $($cachedStats.Updated) | Fejl: $($cachedStats.WithErrors)" -Forgrundsfarve Grå                             Write-Host " Fuldført i $([math]::Round($streamSw.Elapsed.TotalSeconds, 1))s (no processing needed)" -ForegroundColor Green                             returner $cachedStats                         }                     } ellers {                         # DELTA PATCH: Find præcis, hvilke filer ændret                         Write-Host " Registrerede ændringer - identificerer ændrede filer..." -Forgrundsfarve gul                         $changedFiles = [System.Collections.ArrayList]::new()                         $newFiles = [System.Collections.ArrayList]::new()                         foreach ($jf i $jsonFiles) {                             $jfKey = $jf. FullName.ToLowerInvariant()                             if (-not $stOldManifest.ContainsKey($jfKey)) {                                 [void]$newFiles.Add($jf)                             } ellers {                                 $cachedLWT = $stOldManifest[$jfKey]. LastWriteTimeUtc                                 $fileDT = $jf. LastWriteTimeUtc                                 prøv {                                     $cachedDT = if ($cachedLWT -is [DateTime]) { $cachedLWT.ToUniversalTime() } else { [DateTimeOffset]::P arse("$cachedLWT"). UtcDateTime }                                     if ([math]::Abs(($cachedDT - $fileDT). TotalSeconds) -gt 1) { [void]$changedFiles.Add($jf) }                                 } catch { [void]$changedFiles.Add($jf) }                             }                         }                         $totalChanged = $changedFiles.Count + $newFiles.Count                         $changePct = [matematik]::Rund(($totalChanged / $totalFiles) * 100, 1)                         Write-Host " Ændret: $($changedFiles.Count) | Nyt: $($newFiles.Count) | I alt: $totalChanged ($changePct%)" -Forgrundsfarve Gul                         if ($totalChanged -gt 0 -og $changePct -lt 10) {                             # DELTA PATCH MODE: <10% ændret, patch eksisterende data                             Write-Host " Delta patch mode ($changePct% < 10%) - patching $totalChanged filer ..." -ForegroundColor Green                             $dataDir = Join-Path $OutputPath "data"                             # Indlæs ændrede/nye enhedsposter                             $deltaDevices = @{}                             $allDeltaFiles = @($changedFiles) + @($newFiles)                             foreach ($df i $allDeltaFiles) {                                 prøv {                                     $devData = Get-Content $df. FullName -Raw | ConvertFrom-Json                                     $dev = Normalize-DeviceRecord $devData                                     hvis ($dev. HostName) { $deltaDevices[$dev. HostName] = $dev }                                 } fange { }                             }                             Write-Host " Indlæste $($deltaDevices.Count) ændrede enhedsposter" -Forgrundsfarve grå                             # For hver kategori JSON: fjern gamle poster for ændrede værtsnavne, tilføj nye poster                             $categoryFiles = @("fejl", "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[streng]]:new([System.StringComparer]::OrdinalIgnoreCase)                             foreach ($hn i $deltaDevices.Keys) { [void]$changedHostnames.Add($hn) }                             foreach ($cat i $categoryFiles) {                                 $catPath = Join-Path $dataDir "$cat.json"                                 if ($catPath teststi) {                                     prøv {                                         $catData = Get-Content $catPath -Raw | ConvertFrom-Json                                         # Fjern gamle poster for ændrede værtsnavne                                         $catData = @($catData | Where-Object { -not $changedHostnames.Contains($_. HostName) })                                         # Klassificer hver ændret enhed igen i kategorier                                         # (tilføjes nedenfor efter klassificering)                                         $catData | ConvertTo-Json dybde 5 | Set-Content $catPath -Kodning UTF8                                     } fange { }                                 }                             }                             # Klassificer hver ændret enhed, og føj til de rigtige kategorifiler                             foreach ($dev i $deltaDevices.Values) {                                 $slim = [bestilt]@{                                     HostName = $dev. Værtsnavn                                     WMI_Manufacturer = hvis ($dev. PSObject.Properties['WMI_Manufacturer']) { $dev. WMI_Manufacturer } ellers { "" }                                     WMI_Model = hvis ($dev. PSObject.Properties['WMI_Model']) { $dev. WMI_Model } ellers { "" }                                     BucketId = if ($dev. PSObject.Properties['BucketId']) { $dev. BucketId } else { "" }                                     ConfidenceLevel = if ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } ellers { "" }                                     IsUpdated = $dev. IsUpdated                                     UEFICA2023Error = if ($dev. PSObject.Properties['UEFICA2023Error']) { $dev. UEFICA2023Error } else { $null }                                     SecureBootTaskStatus = if ($dev. PSObject.Properties['SecureBootTaskStatus']) { $dev. SecureBootTaskStatus } else { "" }                                     KnownIssueId = if ($dev. PSObject.Properties['KnownIssueId']) { $dev. KnownIssueId } else { $null }                                     SkipReasonKnownIssue = if ($dev. PSObject.Properties['SkipReasonKnownIssue']) { $dev. SkipReasonKnownIssue } else { $null }                                 }                                 $isUpd = $dev. IsUpdated -eq $true                                 $conf = hvis ($dev. PSObject.Properties['ConfidenceLevel']) { $dev. ConfidenceLevel } ellers { "" }                                 $hasErr = (-not [streng]::IsNullOrEmpty($dev. UEFICA2023Error) - og $dev. UEFICA2023Fejl -ne "0" -og $dev. UEFICA2023Fejl -ne "")                                 $tskDis = ($dev. SecureBootTaskEnabled -eq $false -eller $dev. SecureBootTaskStatus -eq 'Disabled' -eller $dev. SecureBootTaskStatus -eq 'NotFound')                                 $tskNF = ($dev. SecureBootTaskStatus -eq 'NotFound')                                 $sbOn = ($dev. SecureBootEnabled -ne $false -and "$($dev. SecureBootEnabled)" -ne "Falsk")                                 $e 1801 = if ($dev. PSObject.Properties['Event1801Count']) { [int]$dev. Hændelse1801Tæl } ellers { 0 }                                 $e 1808 = hvis ($dev. PSObject.Properties['Event1808Count']) { [int]$dev. Hændelse1808Tæl } ellers { 0 }                                 $e 1803 = hvis ($dev. PSObject.Properties['Event1803Count']) { [int]$dev. Hændelse1803Tæl } ellers { 0 }                                 $mKEK = ($e 1803 -gt 0 -eller $dev. MissingKEK -eq $true)                                 $hKI = ((-not [streng]::IsNullOrEmpty($dev. SkipReasonKnownIssue)) -eller (-not [streng]::IsNullOrEmpty($dev. KnownIssueId)))                                 $rStat = hvis ($dev. PSObject.Properties['RolloutStatus']) { $dev. RolloutStatus } else { "" }                                 # Føj til matchende kategorifiler                                 $targets = @()                                 if ($isUpd) { $targets += "updated_devices" }                                 if ($hasErr) { $targets += "errors" }                                 if ($hKI) { $targets += "known_issues" }                                 if ($mKEK) { $targets += "missing_kek" }                                 if (-not $isUpd -and $sbOn) { $targets += "not_updated" }                                 if ($tskDis) { $targets += "task_disabled" }                                 if (-not $isUpd -and ($tskDis -or (Test-ConfidenceLevel $conf 'TemporarilyPaused'))) { $targets += "temp_failures" }                                 if (-not $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -or ($tskNF -and $hasErr))) { $targets += "perm_failures" }                                 if (-not $isUpd -and (Test-ConfidenceLevel $conf 'ActionRequired')) { $targets += "action_required" }                                 if (-not $sbOn) { $targets += "secureboot_off" }                                 if ($e 1801 -gt 0 -and $e 1808 -eq 0 -and -not $hasErr -and $rStat -eq "InProgress") { $targets += "rollout_inprogress" }                                 foreach ($tgt i $targets) {                                     $tgtPath = Join-Path $dataDir "$tgt.json"                                     if (teststi $tgtPath) {                                         $existing = Get-Content $tgtPath -Raw | ConvertFrom-Json                                         $existing = @($existing) + @([PSCustomObject]$slim)                                         $existing | ConvertTo-Json dybde 5 | Set-Content $tgtPath -Kodning UTF8                                     }                                 }                             }                             # Genopret CSV'er fra lappet JSON'er                             Write-Host " Regenerating CSVs from patched data..." -ForegroundColor Gray                             $newTimestamp = Get-Date -Format "yyyyMMdd-HHmmss"                             foreach ($cat i $categoryFiles) {                                 $catJsonPath = Join-Path $dataDir "$cat.json"                                 $catCsvPath = Join-Path $OutputPath "SecureBoot_${cat}_$newTimestamp.csv"                                 if ($catJsonPath teststi) {                                     prøv {                                         $catJsonData = Get-Content $catJsonPath -Raw | ConvertFrom-Json                                         if ($catJsonData.Count -gt 0) {                                             $catJsonData | Export-Csv -Path $catCsvPath -NoTypeInformation -Encoding UTF8                                         }                                     } fange { }                                 }                             }                             # Recount statistik fra den patched JSON filer                             Write-Host " Genberegner oversigt fra fejlrettede data..." -Forgrundsfarve Grå                             $patchedStats = [ordered]@{ ReportGeneratedAt = (Get-Date). ToString("åååå-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 i $categoryFiles) {                                 $catPath = Join-Path $dataDir "$cat.json"                                 $cnt = 0                                 if (Test-Path $catPath) { try { $cnt = (Get-Content $catPath -Raw | ConvertFrom-Json). Count } catch { } }                                 ($cat) {                                     "updated_devices" { $pUpdated = $cnt }                                     "fejl" { $pErrors = $cnt }                                     "known_issues" { $pKI = $cnt }                                     "missing_kek" { $pKEK = $cnt }                                     "not_updated" { } # computer                                     "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 (joinsti $dataDir "not_updated.json") -Raw | ConvertFrom-Json). Tælle                             $pTotal = $pUpdated + $pNotUpdated + $pSBOff                             Write-Host " Deltarettelse fuldført: $totalChanged enheder opdateret" -Forgrundsfarve grøn                             Write-Host " I alt: $pTotal | Opdateret: $pUpdated | NotUpdated: $pNotUpdated | Fejl: $pErrors" -Forgrundsfarve hvid                             # Opdater manifest                             $stManifestDir = Join-Path $OutputPath ".cache"                             $stNewManifest = @{}                             foreach ($jf i $jsonFiles) {                                 $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{                                     LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o"); Størrelse = $jf. Længde                                 }                             }                             Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath                             Write-Host " Fuldført i $([math]::Round($streamSw.Elapsed.TotalSeconds, 1))s (deltarettelse - $totalChanged enheder)" -ForgrundFarve grøn                             # Fall through to full streaming reprocess to regenerate HTML dashboard                             # Datafilerne er allerede rettet, så dette sikrer, at dashboardet forbliver opdateret                             Write-Host " Genopretter dashboard fra reparerede data..." -Forgrundsfarve gul                         } ellers {                             Write-Host " $changePct % filer ændret (>= 10 %) - fuld genbehandling af streaming påkrævet" -Forgrundsfarve gul                         }                     }                 }             }         }         # Opret dataundermappe til JSON-filer på enheder efter behov         $dataDir = Join-Path $OutputPath "data"         if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir -Force | Out-Null }         # Deduplication via HashSet (O(1) pr. opslag, ~50 MB for 600.000 værtsnavne)         $seenHostnames = [System.Collections.Generic.HashSet[streng]]:new([System.StringComparer]::OrdinalIgnoreCase)         # Letvægtsoversigtstællere (erstatter $allDevices + $uniqueDevices i hukommelsen)         $c = @{             I alt = 0; SBEnabled = 0; SBOff = 0             Opdateret = 0; HighConf = 0; UnderObs = 0; ActionReq = 0; TempPaused = 0; NotSupported = 0; NoConfData = 0             TaskDisabled = 0; TaskNotFound = 0; TaskDisabledNotUpdated = 0             WithErrors = 0; InProgress = 0; NotYetInitiated = 0; RolloutInProgress = 0             WithKnownIssues = 0; WithMissingKEK = 0; TempFailures = 0; PermFailures = 0; NeedsReboot = 0             UpdatePending = 0         }         # Bucket tracking for AtRisk/SafeList (lightweight sets)         $stFailedBuckets = [System.Collections.Generic.HashSet[string]]:new()         $stSuccessBuckets = [System.Collections.Generic.HashSet[string]]:new()         $stAllBuckets = @{}         $stMfrCounts = @{}         $stErrorCodeCounts = @{}; $stErrorCodeSamples = @{}         $stKnownIssueCounts = @{}         # Batchtilstandsenhedsdatafiler: Akkumulere pr. segment, tømme ved segmentgrænser         $stDeviceFiles = @("fejl", "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 i $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 (kun vigtige felter, ~ 200 byte vs ~ 2KB fuld)         funktionen Get-SlimDevice {             param($Dev)             returner [bestilt]@{                 HostName = $Dev.HostName                 WMI_Manufacturer = if ($Dev.PSObject.Properties['WMI_Manufacturer']) { $Dev.WMI_Manufacturer } else { "" }                 WMI_Model = if ($Dev.PSObject.Properties['WMI_Model']) { $Dev.WMI_Model } else { "" }                 BucketId = if ($Dev.PSObject.Properties['BucketId']) { $Dev.BucketId } else { "" }                 ConfidenceLevel = if ($Dev.PSObject.Properties['ConfidenceLevel']) { $Dev.ConfidenceLevel } else { "" }                 IsUpdated = $Dev.IsUpdated                 UEFICA2023Error = if ($Dev.PSObject.Properties['UEFICA2023Error']) { $Dev.UEFICA2023Error } else { $null }                 SecureBootTaskStatus = if ($Dev.PSObject.Properties['SecureBootTaskStatus']) { $Dev.SecureBootTaskStatus } else { "" }                 KnownIssueId = if ($Dev.PSObject.Properties['KnownIssueId']) { $Dev.KnownIssueId } 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 }             }         }         # Ryd batch til JSON-fil (tilføjelsestilstand)         funktionen Flush-DeviceBatch {             param([streng]$StreamName, [System.Collections.Generic.List[object]]$Batch)             if ($Batch.Count -eq 0) { return }             $fPath = $stDeviceFilePaths[$StreamName]             $fSb = [System.Text.StringBuilder]:new()             foreach ($fDev i $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)         }         # MAIN STREAMING LOOP         $stChunkSize = if ($totalFiles -le 10000) { $totalFiles } else { 10000 }         $stTotalChunks = [matematik]::Loft($totalFiles / $stChunkSize)         $stPeakMemMB = 0         if ($stTotalChunks -gt 1) {             Write-Host "Behandler $totalFiles filer i $stTotalChunks dele af $stChunkSize (streaming, $ParallelThreads tråde):" -Forgrundsfarve cyan         } ellers {             Write-Host "Behandler $totalFiles filer (streaming, $ParallelThreads tråde):" -Forgrundsfarve cyan         }         for ($ci = 0; $ci -lt $stTotalChunks; $ci++) {             $cStart = $ci * $stChunkSize             $cEnd = [matematik]::Min($cStart + $stChunkSize, $totalFiles) - 1             $cFiles = $jsonFiles[$cStart.. $cEnd]             if ($stTotalChunks -gt 1) {                 Write-Host " Chunk $($ci + 1)/$stTotalChunks ($($cFiles.Count) files): " -NoNewline -ForegroundColor Gray             } ellers {                 Write-Host " Indlæser $($cFiles.Count)-filer: " -NoNewline -ForegroundColor Gray             }             $cSw = [System.Diagnostics.Stopwatch]::StartNew()             $rawDevices = Load-FilesParallel -Files $cFiles -Threads $ParallelThreads             # Batchlister pr. afsnit             $cBatches = @{}             foreach ($df i $stDeviceFiles) { $cBatches[$df] = [System.Collections.Generic.List[object]]:new() }             $cNew = 0; $cDupe = 0             foreach ($raw i $rawDevices) {                 if (-not $raw) { continue }                 $device = Normalize-DeviceRecord $raw                 $hostname = $device. Værtsnavn                 if (-not $hostname) { continue }                 if ($seenHostnames.Contains($hostname)) { $cDupe++; continue }                 [void]$seenHostnames.Add($hostname)                 $cNew++; $c.Total++                 $sbOn = ($device. SecureBootEnabled -ne $false -and "$($device. SecureBootEnabled)" -ne "Falsk")                 if ($sbOn) { $c.SBEnabled++ } else { $c.SBOff++; $cBatches["secureboot_off"]. Add((Get-SlimDevice $device)) }                 $isUpd = $device. IsUpdated -eq $true                 $conf = hvis ($device. PSObject.Properties['ConfidenceLevel'] -and $device. ConfidenceLevel) { "$($device. ConfidenceLevel)" } ellers { "" }                 $hasErr = (-not [streng]::IsNullOrEmpty($device. UEFICA2023Fejl) -og "$($device. UEFICA2023Fejl)" -ne "0" -and "$($device. UEFICA2023Fejl)" -ne "")                 $tskDis = ($device. SecureBootTaskEnabled -eq $false -eller "$($device. SecureBootTaskStatus)" -eq 'Disabled' -eller "$($device. SecureBootTaskStatus)" -eq 'NotFound')                 $tskNF = ("$($device. SecureBootTaskStatus)" -eq 'NotFound')                 $bid = hvis ($device. PSObject.Properties['BucketId'] -and $device. BucketId) { "$($device. BucketId)" } ellers { "" }                 $e 1808 = hvis ($device. PSObject.Properties['Event1808Count']) { [int]$device. Hændelse1808Tæl } ellers { 0 }                 $e 1801 = hvis ($device. PSObject.Properties['Event1801Count']) { [int]$device. Hændelse1801Tæl } ellers { 0 }                 $e 1803 = hvis ($device. PSObject.Properties['Event1803Count']) { [int]$device. Hændelse1803Tæl } ellers { 0 }                 $mKEK = ($e 1803 -gt 0 -eller $device. MissingKEK -eq $true -or "$($device. MissingKEK)" -eq "True")                 $hKI = ((-not [streng]::IsNullOrEmpty($device. SkipReasonKnownIssue)) -eller (-not [streng]::IsNullOrEmpty($device. KnownIssueId)))                 $rStat = hvis ($device. PSObject.Properties['RolloutStatus']) { $device. RolloutStatus } else { "" }                 $mfr = hvis ($device. PSObject.Properties['WMI_Manufacturer'] -and -not [string]::IsNullOrEmpty($device. WMI_Manufacturer)) { $device. WMI_Manufacturer } ellers { "Ukendt" }                 $bid = if (-not [streng]::IsNullOrEmpty($bid)) { $bid } else { "" }                 # Flaget Afventer forudberegning af opdatering (politik/WinCS anvendt, status endnu ikke opdateret, SB TIL, opgave er ikke deaktiveret)                 $uefiStatus = hvis ($device. PSObject.Properties['UEFICA2023Status']) { "$($device. UEFICA2023Status)" } ellers { "" }                 $hasPolicy = ($device. PSObject.Properties['AvailableUpdatesPolicy'] -and $null -ne $device. AvailableUpdatesPolicy -og "$($device. AvailableUpdatesPolicy)" -ne '')                 $hasWinCS = ($device. PSObject.Properties['WinCSKeyApplied'] -and $device. WinCSKeyApplied -eq $true)                 $statusPending = ([streng]::IsNullOrEmpty($uefiStatus) -eller $uefiStatus -eq 'NotStarted' -eller $uefiStatus -eq 'InProgress')                 $isUpdatePending = (($hasPolicy -eller $hasWinCS) -and $statusPending -and -not $isUpd -and $sbOn -and -not $tskDis)                 if ($isUpd) {                     $c.Updated++; [void]$stSuccessBuckets.Add($bid); $cBatches["updated_devices"]. Add((Get-SlimDevice $device))                     # Spor opdaterede enheder, der skal genstartes (UEFICA2023Status=Updated but Event1808=0)                     if ($e 1808 -eq 0) { $c.NeedsReboot++; $cBatches["needs_reboot"]. Add((Get-SlimDevice $device)) }                 }                 elseif (-not $sbOn) {                     # SecureBoot OFF – uden for rækkevidde, klassificeres ikke tillidsfuldt                 }                 ellers {                     if ($isUpdatePending) { } # Counted separate in Update Pending – gensidigt eksklusiv for cirkeldiagram                     elseif (Test-ConfidenceLevel $conf "HighConfidence") { $c.HighConf++ }                     elseif (Test-ConfidenceLevel $conf "UnderObservation") { $c.UnderObs++ }                     elseif (Test-ConfidenceLevel $conf "Midlertidig pause") { $c.TempPaused++ }                     elseif (Test-ConfidenceLevel $conf "Ikke Understøttet") { $c.NotSupported++ }                     else { $c.ActionReq++ }                     if ([streng]::IsNullOrEmpty($conf)) { $c.NoConfData++ }                 }                 if ($tskDis) { $c.TaskDisabled++; $cBatches["task_disabled"]. Add((Get-SlimDevice $device)) }                 if ($tskNF) { $c.TaskNotFound++ }                 if (-not $isUpd -and $tskDis) { $c.TaskDisabledNotUpdated++ }                 if ($hasErr) {                     $c.WithErrors++; [void]$stFailedBuckets.Add($bid); $cBatches["fejl"]. Add((Get-SlimDevice $device))                     $ec = $device. UEFICA2023Fejl                     if (-not $stErrorCodeCounts.ContainsKey($ec)) { $stErrorCodeCounts[$ec] = 0; $stErrorCodeSamples[$ec] = @() }                     $stErrorCodeCounts[$ec]++                     if ($stErrorCodeSamples[$ec]. Count -lt 5) { $stErrorCodeSamples[$ec] += $hostname }                 }                 if ($hKI) {                     $c.WithKnownIssues++; $cBatches["known_issues"]. Add((Get-SlimDevice $device))                     $ki = if (-not [streng]::IsNullOrEmpty($device. SkipReasonKnownIssue)) { $device. SkipReasonKnownIssue } else { $device. KnownIssueId }                     if (-not $stKnownIssueCounts.ContainsKey($ki)) { $stKnownIssueCounts[$ki] = 0 }; $stKnownIssueCounts[$ki]++                 }                 if ($mKEK) { $c.WithMissingKEK++; $cBatches["missing_kek"]. Add((Get-SlimDevice $device)) }                 if (-not $isUpd -and ($tskDis -or (Test-ConfidenceLevel $conf 'TemporarilyPaused'))) { $c.TempFailures++; $cBatches["temp_failures"]. Add((Get-SlimDevice $device)) }                 if (-not $isUpd -and ((Test-ConfidenceLevel $conf 'NotSupported') -or ($tskNF -and $hasErr))) { $c.PermFailures++; $cBatches["perm_failures"]. Add((Get-SlimDevice $device)) }                 if ($e 1801 -gt 0 -og $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++ }                 # Opdatering afventer: politik eller WinCS anvendt, status venter, SB ON, opgave ikke deaktiveret                 if ($isUpdatePending) {                     $c.UpdatePending++; $cBatches["update_pending"]. Add((Get-SlimDevice $device))                 }                 if (-not $isUpd -and $sbOn) { $cBatches["not_updated"]. Add((Get-SlimDevice $device)) }                 # Under Observationsenheder (adskilt fra Handling påkrævet)                 if (-not $isUpd -and (Test-ConfidenceLevel $conf 'UnderObservation')) { $cBatches["under_observation"]. Add((Get-SlimDevice $device)) }                 # Handling Påkrævet: ikke-opdateret, SB ON, ikke matchende andre tillidskategorier, ikke Opdater ventende                 if (-not $isUpd -and $sbOn -and -not $isUpdatePending -and -not (Test-ConfidenceLevel $conf 'HighConfidence') -and -not (Test-ConfidenceLevel $conf 'UnderObservation') -and -not (Test-ConfidenceLevel $conf 'Midlertidig pause') -and-not (Test-ConfidenceLevel $conf 'NotSupported')) {                     $cBatches["action_required"]. Add((Get-SlimDevice $device))                 }                 if (-not $stMfrCounts.ContainsKey($mfr)) { $stMfrCounts[$mfr] = @{ Total=0; Updated=0; UpdatePending=0; HighConf=0; UnderObs=0; ActionReq=0; TempPaused=0; NotSupported=0; SBOff=0; WithErrors=0 } }                 $stMfrCounts[$mfr]. Total++                 if ($isUpd) { $stMfrCounts[$mfr]. Opdateret++ }                 elseif (-not $sbOn) { $stMfrCounts[$mfr]. SBOff++ }                 elseif ($isUpdatePending) { $stMfrCounts[$mfr]. UpdatePending++ }                 elseif (Test-ConfidenceLevel $conf "HighConfidence") { $stMfrCounts[$mfr]. HighConf++ }                 elseif (Test-ConfidenceLevel $conf "UnderObservation") { $stMfrCounts[$mfr]. UnderObs++ }                 elseif (Test-ConfidenceLevel $conf "Midlertidig pause") { $stMfrCounts[$mfr]. TempPaused++ }                 elseif (Test-ConfidenceLevel $conf "Ikke Understøttet") { $stMfrCounts[$mfr]. NotSupported++ }                 else { $stMfrCounts[$mfr]. ActionReq++ }                 if ($hasErr) { $stMfrCounts[$mfr]. WithErrors++ }                 # Spor alle enheder efter bucket (herunder tomme BucketId)                 $bucketKey = if ($bid -and $bid -ne "") { $bid } else { "(empty)" }                 if (-not $stAllBuckets.ContainsKey($bucketKey)) {                     $stAllBuckets[$bucketKey] = @{ Count=0; Updated=0; Manufacturer=$mfr; Model=""; BIOS="" }                     hvis ($device. PSObject.Properties['WMI_Model']) { $stAllBuckets[$bucketKey]. Model = $device. WMI_Model }                     hvis ($device. PSObject.Properties['BIOSDescription']) { $stAllBuckets[$bucketKey]. BIOS = $device. BIOSDescription }                 }                 $stAllBuckets[$bucketKey]. Tæl++                 if ($isUpd) { $stAllBuckets[$bucketKey]. Opdateret++ }             }             # Ryd batches til disk             foreach ($df i $stDeviceFiles) { Flush-DeviceBatch -StreamName $df -Batch $cBatches[$df] }             $rawDevices = $null; $cBatches = $null; [System.GC]::Collect()             $cSw.Stop()             $cTime = [Matematik]::Rund($cSw.Forløbet.TotalSeconds, 1)             $cRem = $stTotalChunks - $ci - 1             $cEta = if ($cRem -gt 0) { " | ETA: ~$([Matematik]::Rund($cRem * $cSw.Forløbet.TotalSeconds / 60, 1)) min" } ellers { "" }             $cMem = [matematik]::Rund([System.GC]::GetTotalMemory($false) / 1 MB, 0)             if ($cMem -gt $stPeakMemMB) { $stPeakMemMB = $cMem }             Write-Host " +$cNew ny, $cDupe dupes, ${cTime}s | Mem: ${cMem}MB$cEta" -Forgrundsfarve grøn         }         # Færdiggør JSON-matrixer         foreach ($dfName i $stDeviceFiles) {             [System.IO.File]::AppendAllText($stDeviceFilePaths[$dfName], "'n]", [System.Text.Encoding]::UTF8)             Write-Host " $dfName.json: $($stDeviceFileCounts[$dfName]) devices" -ForegroundColor DarkGray         }         # Compute afledt statistik         $stAtRisk = 0; $stSafeList = 0         foreach ($bid i $stAllBuckets.Keys) {             $b = $stAllBuckets[$bid]; $nu = $b.Count – $b.Opdateret             if ($stFailedBuckets.Contains($bid)) { $stAtRisk += $nu }             elseif ($stSuccessBuckets.Contains($bid)) { $stSafeList += $nu }         }         $stAtRisk = [math]::Max(0, $stAtRisk - $c.WithErrors)         # NotUptodate = count from not_updated batch (enheder med SB ON og ikke opdateret)         $stNotUptodate = $stDeviceFileCounts["not_updated"]         $stats = [bestilt]@{             ReportGeneratedAt = (Get-Date). ToString("åååå-MM-dd HH:mm:ss")             TotalDevices = $c.Total; SecureBootEnabled = $c.SBEnabled; SecureBootOFF = $c.SBOff             Updated = $c.Updated; HighConfidence = $c.HighConf; UnderObservation = $c.UnderObs             ActionRequired = $c.ActionReq; TemporarilyPaused = $c.TempPaused; NotSupported = $c.NotSupported             NoConfidenceData = $c.NoConfData; TaskDisabled = $c.TaskDisabled; TaskNotFound = $c.TaskNotFound             TaskDisabledNotUpdated = $c.TaskDisabledNotUpdated             CertifikaterOpdatering = $c.Opdateret. NotUptodate = $stNotUptodate; FullyUpdated = $c.Updated             OpdateringerPending = $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; Permanente fejl = $c.PermFailures             NeedsReboot = $c.NeedsReboot; UpdatePending = $c.UpdatePending             AtRiskDevices = $stAtRisk; SafeListDevices = $stSafeList             PercentWithErrors = if ($c.Total -gt 0) { [math]::Round(($c.WithErrors/$c.Total)*100,2) } else { 0 }             PercentAtRisk = if ($c.Total -gt 0) { [math]::Round(($stAtRisk/$c.Total)*100,2) } ellers { 0 }             PercentSafeList = if ($c.Total -gt 0) { [math]::Round(($stSafeList/$c.Total)*100,2) } else { 0 }             PercentHighConfidence = if ($c.Total -gt 0) { [math]::Round(($c.HighConf/$c.Total)*100,1) } else { 0 }             PercentCertUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 }             PercentActionRequired = if ($c.Total -gt 0) { [math]::Round(($c.ActionReq/$c.Total)*100,1) } else { 0 }             PercentNotUptodate = if ($c.Total -gt 0) { [math]::Round($stNotUptodate/$c.Total*100,1) } ellers { 0 }             PercentFullyUpdated = if ($c.Total -gt 0) { [math]::Round(($c.Updated/$c.Total)*100,1) } else { 0 }             UniqueBuckets = $stAllBuckets.Count; PeakMemoryMB = $stPeakMemMB; ProcessingMode = "Streaming"         }         # Skriv CSV'er         [PSCustomObject]$stats | Export-Csv -Path (joinsti $OutputPath "SecureBoot_Summary_$timestamp.csv") -NoTypeInformation -Encoding UTF8         $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Faldende | ForEach-Object {             [PSCustomObject]@{ Manufacturer=$_. Nøglen; Count=$_. Value.Total; Opdateret=$_. Value.Updated. HighConfidence=$_. Value.HighConf; ActionRequired=$_. Value.ActionReq }         } | Export-Csv -Path (Join-Path $OutputPath "SecureBoot_ByManufacturer_$timestamp.csv") -NoTypeInformation -Encoding UTF8         $stErrorCodeCounts.GetEnumerator() | Sort-Object værdi - faldende | ForEach-Object {             [PSCustomObject]@{ ErrorCode=$_. Nøglen; Count=$_. Værdi; SampleDevices=($stErrorCodeSamples[$_. Nøgle] -join ", ") }         } | Export-Csv -Path (joinsti $OutputPath "SecureBoot_ErrorCodes_$timestamp.csv") -NoTypeInformation -Encoding UTF8         $stAllBuckets.GetEnumerator() | Sort-Object { $_. Value.Count } -Faldende | ForEach-Object {             [PSCustomObject]@{ BucketId=$_. Nøglen; Count=$_. Value.Count; Opdateret=$_. Value.Updated. NotUpdated=$_. Value.Count-$_. Value.Updated. Manufacturer=$_. Value.Manufacturer }         } | Export-Csv -Path (joinsti $OutputPath "SecureBoot_UniqueBuckets_$timestamp.csv") -NoTypeInformation -Encoding UTF8         # Generer orchestrator-kompatible CSV'er (forventede filnavne for Start-SecureBootRolloutOrchestrator.ps1)         $notUpdatedJsonPath = Join-Path $dataDir "not_updated.json"         if (teststi $notUpdatedJsonPath) {             prøv {                 $nuData = Get-Content $notUpdatedJsonPath -Raw | ConvertFrom-Json                 if ($nuData.Count -gt 0) {                     # NotUptodate CSV – orchestrator søger efter *NotUptodate*.csv                     $nuData | Export-Csv -Path (joinsti $OutputPath "SecureBoot_NotUptodate_$timestamp.csv") -NoTypeInformation -Encoding UTF8                     Write-Host " Orchestrator CSV: SecureBoot_NotUptodate_$timestamp.csv ($($nuData.Count)" -Forgrundsfarve Grå                 }             } fange { }         }         # Skriv JSON-data til dashboard         $stats | ConvertTo-Json dybde 3 | Set-Content (joinsti-$dataDir "summary.json") -kodning AF UTF8         # HISTORISK REGISTRERING: Gemme datapunkt til tendensdiagram         # Brug en stabil cacheplacering, så tendensdata bevares på tværs af tidsstemplede sammenlægningsmapper.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   # Hvis OutputPath ligner "...\Aggregation_yyyyMMdd_HHmmss", placeres cachen i den overordnede mappe.# Ellers går cachen ind i selve OutputPath.$parentDir = Split-Path $OutputPath -Parent         $leafName = Split-Path $OutputPath -Blad         if ($leafName -match '^Aggregation_\d{8}' -eller $leafName -eq 'Aggregation_Current') {             # Orchestrator-oprettet tidsstemplet mappe – brug overordnet til stabil cache             $historyPath = Join-Path $parentDir ".cache\trend_history.json"         } ellers {             $historyPath = Join-Path $OutputPath ".cache\trend_history.json"         }         $historyDir = Split-Path $historyPath -Parent         if (-not (Test-Path $historyDir)) { New-Item -ItemType Directory -Path $historyDir -Force | Out-Null }         $historyData = @()         if (teststi $historyPath) {             prøv { $historyData = @(Get-Content $historyPath -Raw | ConvertFrom-Json) } fangst { $historyData = @() }         }         # Kontrollér også i OutputPath\.cache\ (ældre placering fra ældre versioner)         # Flet datapunkter, der ikke allerede findes i den primære historik         if ($leafName -eq 'Aggregation_Current' -or $leafName -match '^Aggregation_\d{8}') {             $innerHistoryPath = Join-Path $OutputPath ".cache\trend_history.json"             if ((Test-Path $innerHistoryPath) -and $innerHistoryPath -ne $historyPath) {                 prøv {                     $innerData = @(Get-Content $innerHistoryPath -Raw | ConvertFrom-Json)                     $existingDates = @($historyData | ForEach-Object { $_. Dato })                     foreach ($entry i $innerData) {                         hvis ($entry. Dato og $entry. Dato -notin $existingDates) {                             $historyData += $entry                         }                     }                     if ($innerData.Count -gt 0) {                         Write-Host " Flettede $($innerData.Count) datapunkter fra indre cache" -Forgrundsfarve DarkGray                     }                 } fange { }             }         }

# BOOTSTRAP: Hvis tendenshistorikken er tom/sparse, rekonstrueres ud fra historiske data         if ($historyData.Count -lt 2 -and ($leafName -match '^Aggregation_\d{8}' -or $leafName -eq 'Aggregation_Current')) {             Write-Host " Bootstrapping tendenshistorik fra historiske data ..." -Forgrundsfarve Gul             $dailyData = @{}                          # Kilde 1: Oversigts-CSV'er i den aktuelle mappe (Aggregation_Current beholder alle oversigts-CSV'er)             $localSummaries = Get-ChildItem $OutputPath -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Sort-Object navn             foreach ($summCsv i $localSummaries) {                 prøv {                     $summ = Import-Csv $summCsv.FullName | Select-Object -Første 1                     hvis ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0 -og $summ. ReportGeneratedAt) {                         $dateStr = ([datetime]$summ. ReportGeneratedAt). ToString("åååå-MM-dd")                         $updated = hvis ($summ. Opdateret) { [int]$summ. Opdateret } ellers { 0 }                         $notUpd = if ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated }                         $dailyData[$dateStr] = [PSCustomObject]@{                             Date = $dateStr; Total = [int]$summ. TotalDevices; Opdateret = $updated; NotUpdated = $notUpd                             NeedsReboot = 0; Fejl = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } else { 0 }                         }                     }                 } fange { }             }                          # Kilde 2: Gamle tidsstemplede Aggregation_* mapper (ældre, hvis de stadig findes)             $aggFolders = Get-ChildItem $parentDir -Directory -Filter "Aggregation_*" -EA SilentlyContinue |                 Where-Object { $_. Navn -match '^Aggregation_\d{8}' } |                 Sort-Object navn             foreach ($folder i $aggFolders) {                 $summCsv = Get-ChildItem $folder. FullName -Filter "SecureBoot_Summary_*.csv" -EA SilentlyContinue | Select-Object -Første 1                 if ($summCsv) {                     prøv {                         $summ = Import-Csv $summCsv.FullName | Select-Object -Første 1                         hvis ($summ. TotalDevices -and [int]$summ. TotalDevices -gt 0) {                             $dateStr = $folder. Navn -erstat '^Aggregation_(\d{4})(\d{2})(\d{2})_.*', '$1-$2-$3'                             $updated = hvis ($summ. Opdateret) { [int]$summ. Opdateret } ellers { 0 }                             $notUpd = if ($summ. NotUptodate) { [int]$summ. NotUptodate } else { [int]$summ. TotalDevices - $updated }                             $dailyData[$dateStr] = [PSCustomObject]@{                                 Date = $dateStr; Total = [int]$summ. TotalDevices; Opdateret = $updated; NotUpdated = $notUpd                                 NeedsReboot = 0; Fejl = 0; ActionRequired = if ($summ. ActionRequired) { [int]$summ. ActionRequired } else { 0 }                             }                         }                     } fange { }                 }             }                          # Kilde 3: RolloutState.json WaveHistory (har per-bølge tidsstempler fra dag 1)             # Dette giver datapunkter for grundlinjer, selvom der ikke findes nogen gamle sammenlægningsmapper             $rolloutStatePaths = @(                 (Join-Path-$parentDir "RolloutState\RolloutState.json"),                 (Joinsti-$OutputPath "RolloutState\RolloutState.json")             )             foreach ($rsPath i $rolloutStatePaths) {                 if (teststi $rsPath) {                     prøv {                         $rsData = Get-Content $rsPath -Raw | ConvertFrom-Json                         if ($rsData.WaveHistory) {                             # Brug bølgestartdatoer som tendensdatapunkter                             # Beregn kumulative enheder målrettet på hver bølge                             $cumulativeTargeted = 0                             foreach ($wave i $rsData.WaveHistory) {                                 hvis ($wave. StartedeAt -og $wave. DeviceCount) {                                     $waveDate = ([datetime]$wave. StartedAt). ToString("åååå-MM-dd")                                     $cumulativeTargeted += [int]$wave. DeviceCount                                     if (-not $dailyData.ContainsKey($waveDate)) {                                         #Omtrentlig: Ved bølgestarttidspunkt er det kun enheder fra tidligere bølger, der blev opdateret                                         $dailyData[$waveDate] = [PSCustomObject]@{                                             Date = $waveDate; Total = $c.Total; Opdateret = [matematik]:Max(0, $cumulativeTargeted - [int]$wave. DeviceCount)                                             NotUpdated = $c.Total - [math]::Max(0, $cumulativeTargeted - [int]$wave. DeviceCount)                                             NeedsReboot = 0; Fejl = 0; ActionRequired = 0                                         }                                     }                                 }                             }                         }                     } fange { }                     pause # Brug først fundet                 }             }

if ($dailyData.Count -gt 0) {                 $historyData = @($dailyData.GetEnumerator() | Sort-Object nøgle | ForEach-Object { $_. Værdi })                 Write-Host " Bootstrapped $($historyData.Count) datapunkter fra historiske oversigter" -Forgrundsfarve Grøn             }         }

# Tilføj aktuelt datapunkt (dedupliker efter dag – behold seneste pr. dag)         $todayKey = (Hent-dato). ToString("åååå-MM-dd")         $existingToday = $historyData | Where-Object { "$($_. Date)" -like "$todayKey*" }         if ($existingToday) {             # Erstat dagens indtastning             $historyData = @($historyData | Where-Object { "$($_. Date)" -notlike "$todayKey*" })         }         $historyData += [PSCustomObject]@{             Date = $todayKey             Total = $c.Total             Opdateret = $c.Updated             NotUpdated = $stNotUptodate             NeedsReboot = $c.NeedsReboot             Fejl = $c.WithErrors             ActionRequired = $c.ActionReq         }         # Fjern ugyldige datapunkter (0 i alt), og behold de sidste 90         $historyData = @($historyData | Where-Object { [int]$_. I alt -gt 0 })         # No cap – tendensdata er ~100 byte/entry, et helt år = ~36 KB         $historyData | ConvertTo-Json dybde 3 | Set-Content $historyPath -Kodning UTF8         Write-Host " Tendenshistorik: $($historyData.Count) datapunkter" -Forgrundsfarve DarkGray                  # Opbyg tendensdiagramdata til HTML         $trendLabels = ($historyData | ForEach-Object { "'$($_. Date)'" }) -join ","         $trendUpdated = ($historyData | ForEach-Object { $_. Opdateret }) -join ","         $trendNotUpdated = ($historyData | ForEach-Object { $_. NotUpdated }) -join ","         $trendTotal = ($historyData | ForEach-Object { $_. Total }) -join ","         # Projicering: udvid tendenslinjen ved hjælp af eksponentiel fordobling (2,4,8,16...)         # Afleder bølgestørrelse og observationsperiode fra faktiske data fra tendenshistorikken.# - Bølgestørrelse = største stigning i en enkelt periode set i historien (den seneste bølge installeret)         # - Observationsdage = gennemsnitlige kalenderdage mellem tendensdatapunkter (hvor ofte vi kører)         # Så fordobler bølgestørrelsen hver periode, matchende orchestrator's 2x vækst strategi.$projLabels = ""; $projUpdated = ""; $projNotUpdated = ""; $hasProjection = $false         if ($historyData.Count -ge 2) {             $lastUpdated = $c.Opdateret             $remaining = $stNotUptodate # Kun SB-ON ikke-opdaterede enheder (udelukker SecureBoot OFF)             $projDates = @(); $projValues = @(); $projNotUpdValues = @()             $projDate = Get-Date

# Afledte bølgestørrelse og observationsperiode fra tendenshistorien             $increments = @()             $dayGaps = @()             for ($hi = 1; $hi -lt $historyData.Count; $hi++) {                 $inc = $historyData[$hi]. Opdateret – $historyData[$hi-1]. Opdateret                 if ($inc -gt 0) { $increments += $inc }                 prøv {                     $d 1 = [datetime]::P arse($historyData[$hi-1]. Dato)                     $d 2 = [datetime]::P arse($historyData[$hi]. Dato)                     $gap = ($d 2 - $d 1). TotalDays                     if ($gap -gt 0) { $dayGaps += $gap }                 } fange {}             }             # Bølgestørrelse = seneste positive forøgelse (aktuel bølge), fallback til gennemsnit, minimum 2             $waveSize = if ($increments. Antal -gt 0) {                 [matematik]::Maks(2, $increments[-1])             } ellers { 2 }             # Observationsperiode = gennemsnitlig afstand mellem datapunkter (kalenderdage pr. bølge), minimum 1             $waveDays = if ($dayGaps.Count -gt 0) {                 [math]::Max(1, [math]::Round(($dayGaps | Measure-Object -Average). Gennemsnit, 0))             } ellers { 1 }

            Write-Host " Projicering: waveSize=$waveSize (fra sidste interval), waveDays=$waveDays (avg gap from history)" -ForegroundColor DarkGray

$dayCounter = 0             # Projicer, indtil alle enheder er opdateret, eller maks. 365 dage             for ($pi = 1; $pi -le 365; $pi++) {                 $projDate = $projDate.AddDays(1)                 $dayCounter++                 # Udrul en bølge ved hver observationsperiode, og dobbelt så                 if ($dayCounter -ge $waveDays) {                     $devicesThisWave = [matematik]::Min($waveSize, $remaining)                     $lastUpdated += $devicesThisWave                     $remaining -= $devicesThisWave                     if ($lastUpdated -gt ($c.Updated + $stNotUptodate)) { $lastUpdated = $c.Updated + $stNotUptodate; $remaining = 0 }                     # Dobbelt bølgestørrelse for næste periode (Orchestrator 2x-strategi)                     $waveSize = $waveSize * 2                     $dayCounter = 0                 }                 $projDates += "'$($projDate.ToString("åååå-MM-dd"))'"                 $projValues += $lastUpdated                 $projNotUpdValues += [matematik]::Maks(0, $remaining)                 if ($remaining -le 0) { break }             }             $projLabels = $projDates -join ","             $projUpdated = $projValues -join ","             $projNotUpdated = $projNotUpdValues -join ","             $hasProjection = $projDates.Count -gt 0         } elseif ($historyData.Count -eq 1) {             Write-Host " Projicering: skal bruge mindst 2 tendensdatapunkter for at udlede bølgetid" -Forgrundsfarve DarkGray         }         # Opbyg kombinerede diagramdatastrenge for her-strengen         $allChartLabels = if ($hasProjection) { "$trendLabels,$projLabels" } ellers { $trendLabels }         $projDataJS = if ($hasProjection) { $projUpdated } else { "" }         $projNotUpdJS = hvis ($hasProjection) { $projNotUpdated } ellers { "" }         $histCount = ($historyData | Måleobjekt). Tælle         $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Faldende | ForEach-Object {             @{ name=$_. Nøglen; total=$_. Value.Total; updated=$_. Value.Updated. highConf=$_. Value.HighConf; actionReq=$_. Value.ActionReq }         } | ConvertTo-Json dybde 3 | Set-Content (joinsti $dataDir "manufacturers.json") -Kodning UTF8         # Konvertér JSON-datafiler til CSV til læsbare Excel-downloads         Write-Host "Konvertering af enhedsdata til CSV til Excel download..." -Forgrundsfarve Grå         foreach ($dfName i $stDeviceFiles) {             $jsonFile = Join-Path $dataDir "$dfName.json"             $csvFile = Join-Path $OutputPath "SecureBoot_${dfName}_$timestamp.csv"             if ($jsonFile teststi) {                 prøv {                     $jsonData = Get-Content $jsonFile -Raw | ConvertFrom-Json                     if ($jsonData.Count -gt 0) {                         # Medtag ekstra kolonner til update_pending CSV                         $selectProps = hvis ($dfName -eq "update_pending") {                             @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Status', 'UEFICA2023Error', 'AvailableUpdatesPolicy', 'WinCSKeyApplied', 'SecureBootTaskStatus')                         } ellers {                             @('HostName', 'WMI_Manufacturer', 'WMI_Model', 'BucketId', 'ConfidenceLevel', 'IsUpdated', 'UEFICA2023Error', 'SecureBootTaskStatus', 'KnownIssueId', 'SkipReasonKnownIssue')                         }                         $jsonData | Select-Object $selectProps |                             Export-Csv -Path $csvFile -NoTypeInformation -Encoding UTF8                         Write-Host " $dfName -> $($jsonData.Count) rækker -> CSV" -Forgrundsfarve DarkGray                     }                 } catch { Write-Host " $dfName - skipped" -ForegroundColor DarkYellow }             }         }         # Generér selvstændigt HTML-dashboard         $htmlPath = Join-Path $OutputPath "SecureBoot_Dashboard_$timestamp.html"         Write-Host "Genererer selvstændig HTML-dashboard..." -Forgrundsfarve Gul         # VELOCITY PROJECTION: Beregn ud fra scanningsoversigten eller tidligere oversigt         $stDeadline = [datetime]"2026-06-24" # KEK cert expiry         $stDaysToDeadline = [math]::Max(0, ($stDeadline - (Hent-dato)). Dage)         $stDevicesPerDay = 0         $stProjectedDate = $null         $stVelocitySource = "I/T"         $stWorkingDays = 0         $stCalendarDays = 0         # Prøv tendenshistorikken først (letvægts, der allerede vedligeholdes af aggregator – erstatter oppustet ScanHistory.json)         if ($historyData.Count -ge 2) {             $validHistory = @($historyData | Where-Object { [int]$_. I alt -gt 0 -og [int]$_. Opdateret -ge 0 })             if ($validHistory.Count -ge 2) {                 $prev = $validHistory[-2]; $curr = $validHistory[-1]                 $prevDate = [datetime]::P arse($prev. Date.Substring(0, [Math]::Min(10, $prev. Date.Length)))                 $currDate = [datetime]::P arse($curr. Date.Substring(0, [Math]::Min(10, $curr. Date.Length)))                 $daysDiff = ($currDate - $prevDate). TotalDays                 if ($daysDiff -gt 0) {                     $updDiff = [int]$curr. Opdateret – [int]$prev. Opdateret                     if ($updDiff -gt 0) {                         $stDevicesPerDay = [matematik]::Rund($updDiff / $daysDiff; 0)                         $stVelocitySource = "TrendHistory"                     }                 }             }         }         # Prøv oversigt over implementeringen af Orchestrator (har forudberegnet hastighed)         if ($stVelocitySource -eq "N/A" -and $RolloutSummaryPath -and (Test-Path $RolloutSummaryPath)) {             prøv {                 $rolloutSummary = Get-Content $RolloutSummaryPath -Raw | ConvertFrom-Json                 if ($rolloutSummary.DevicesPerDay -and [double]$rolloutSummary.DevicesPerDay -gt 0) {                     $stDevicesPerDay = [math]::Round([double]$rolloutSummary.DevicesPerDay, 1)                     $stVelocitySource = "Orchestrator"                     if ($rolloutSummary.ProjectedCompletionDate) {                         $stProjectedDate = $rolloutSummary.ProjectedCompletionDate                     }                     if ($rolloutSummary.WorkingDaysRemaining) { $stWorkingDays = [int]$rolloutSummary.WorkingDaysRemaining }                     if ($rolloutSummary.CalendarDaysRemaining) { $stCalendarDays = [int]$rolloutSummary.CalendarDaysRemaining }                 }             } fange { }         }         # Fallback: Prøv forrige oversigts-CSV (søg i aktuel mappe OG overordnede/sidestillede sammenlægningsmapper)         if ($stVelocitySource -eq "I/T") {             $searchPaths = @(                 (Joinsti-$OutputPath "SecureBoot_Summary_*.csv")             )             # Søg også i sidestillede sammenlægningsmapper (Orchestrator opretter en ny mappe hver kørsel)             $parentPath = Split-Path $OutputPath -Parent             if ($parentPath) {                 $searchPaths += (joinsti $parentPath "Aggregation_*\SecureBoot_Summary_*.csv")                 $searchPaths += (joinsti $parentPath "SecureBoot_Summary_*.csv")             }             $prevSummary = $searchPaths | ForEach-Object { Get-ChildItem $_ -EA SilentlyContinue } | Sort-Object LastWriteTime -Faldende | Select-Object -Første 1             if ($prevSummary) {                 prøv {                     $prevStats = Get-Content $prevSummary.FullName | ConvertFrom-Csv                     $prevDate = [datetime]$prevStats.ReportGeneratedAt                     $daysSinceLast = ((Hent-dato) - $prevDate). TotalDays                     if ($daysSinceLast -gt 0,01) {                         $prevUpdated = [int]$prevStats.Updated                         $updDelta = $c.Opdateret – $prevUpdated                         if ($updDelta -gt 0) {                             $stDevicesPerDay = [matematik]::Rund($updDelta / $daysSinceLast, 0)                             $stVelocitySource = "PreviousReport"                         }                     }                 } fange { }             }         }         # Fallback: beregn hastighed fra hele tendenshistorikkens span (første vs. seneste datapunkt)         if ($stVelocitySource -eq "N/A" -and $historyData.Count -ge 2) {             $validHistory = @($historyData | Where-Object { [int]$_. I alt -gt 0 -og [int]$_. Opdateret -ge 0 })             if ($validHistory.Count -ge 2) {                 $first = $validHistory[0]                 $last = $validHistory[-1]                 $firstDate = [datetime]::P arse($first. Date.Substring(0, [Math]::Min(10, $first. Date.Length)))                 $lastDate = [datetime]::P arse($last. Date.Substring(0, [Math]::Min(10, $last. Date.Length)))                 $daysDiff = ($lastDate - $firstDate). TotalDays                 if ($daysDiff -gt 0) {                     $updDiff = [int]$last. Opdateret – [int]$first. Opdateret                     if ($updDiff -gt 0) {                         $stDevicesPerDay = [matematik]::Rund($updDiff / $daysDiff, 1)                         $stVelocitySource = "TrendHistory"                     }                 }             }         }         # Beregn projicering ved hjælp af eksponentiel dobling (i overensstemmelse med tendensdiagram)         # Genbrug de projiceringsdata, der allerede er beregnet for diagrammet, hvis de er tilgængelige         if ($hasProjection -and $projDates.Count -gt 0) {             # Brug den senest projicerede dato (når alle enheder opdateres)             $lastProjDateStr = $projDates[-1] -replace "'", ""             $stProjectedDate = ([datetime]::P arse($lastProjDateStr)). ToString("MMM dd, åååå")             $stCalendarDays = ([datetime]::P arse($lastProjDateStr) - (Hent-dato)). Dage             $stWorkingDays = 0             $d = Hent dato             for ($i = 0; $i -lt $stCalendarDays; $i++) {                 $d = $d.AddDays(1)                 if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ }             }         } elseif ($stDevicesPerDay -gt 0 -and $stNotUptodate -gt 0) {             # Fallback: lineær projicering, hvis der ikke findes eksponentielle data             $daysNeeded = [matematik]::Loft($stNotUptodate / $stDevicesPerDay)             $stProjectedDate = (Hent-dato). AddDays($daysNeeded). ToString("MMM dd, åååå")             $stWorkingDays = 0; $stCalendarDays = $daysNeeded             $d = Hent dato             for ($i = 0; $i -lt $daysNeeded; $i++) {                 $d = $d.AddDays(1)                 if ($d.DayOfWeek -ne 'Saturday' -and $d.DayOfWeek -ne 'Sunday') { $stWorkingDays++ }             }         }         # Build-hastighed HTML         $velocityHtml = hvis ($stDevicesPerDay -gt 0) {             "<div><stærk>&#128640; Enheder/dag:</strong> $($stDevicesPerDay.ToString('N0')) (kilde: $stVelocitySource)</div>" +             "<><stærk>&#128197; Forventet fuldførelse:</strong> $stProjectedDate" +             $(if ($stProjectedDate -and [datetime]::P arse($stProjectedDate) -gt $stDeadline) { " <span style='color:#dc3545; font-weight:bold'>&#9888; DEADLINE</span>" } else { " <span style='color:#28a745'>&#10003; Inden deadline</span>" }) +             "</div>" +             "<><stærk>&#128336; Arbejdsdage:</strong> $stWorkingDays | <stærk>Calendar Dage:</strong> $stCalendarDays</div>" +             "<div style='font-size:.8em; color:#888'>Deadline: 24. juni 2026 (udløb af KEK-certifikat) | Resterende dage: $stDaysToDeadline</div>"         } ellers {             "<div style='padding:8px; baggrund:#fff3cd; kantradius:4px; kant til venstre:3px massiv #ffc107'>" +             "<stærk>&#128197; Forventet fuldførelse:</strong> Utilstrækkelige data til beregning af hastighed.                                                                                  " +             "Kør sammenlægning mindst to gange med dataændringer for at fastlægge en hastighed.<br/>" +             "<stærk>Deadline:</strong> 24. juni 2026 (udløb af KEK-certifikat) | <stærke>resterende dage:</strong> $stDaysToDeadline</div>"         }                  # Nedtælling for certifikatudløb         $certToday = Hent dato         $certKekExpiry = [datetime]"2026-06-24"         $certUefiExpiry = [datetime]"2026-06-27"         $certPcaExpiry = [datetime]"2026-10-19"         $daysToKek = [matematik]::Maks.(0, ($certKekExpiry - $certToday). Dage)         $daysToUefi = [math]::Max(0, ($certUefiExpiry - $certToday). Dage)         $daysToPca = [matematik]::Maks.(0, ($certPcaExpiry - $certToday). Dage)         $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } ellers { '#28a745' }                  # Helper: Læs poster fra JSON, build bucket oversigt + første N-enhedsrækker         $maxInlineRows = 200         funktionen Build-InlineTable {             param([streng]$JsonPath, [int]$MaxRows = 200, [streng]$CsvFileName = "")             $bucketSummary = ""             $deviceRows = ""             $totalCount = 0             if ($JsonPath teststi) {                 prøv {                     $data = Get-Content $JsonPath -Raw | ConvertFrom-Json                     $totalCount = $data. Tælle                                          # BUCKET SUMMARY: Gruppér efter BucketId, vis antal pr. bucket med opdateret fra global bucket-statistik                     if ($totalCount -gt 0) {                         $buckets = $data | Group-Object BucketId | Sort-Object antal -faldende                         $bucketSummary = "><2 h3 style='font-size:.95em; farve:#333; margin:10px 0 5px'><3 efter hardwarespand ($($buckets. Antal) buckets)><4 /h3>"                         $bucketSummary += "><6 div style='max-height:300px; overløb-y:auto; margen-bund:15px'><tabel><thead><tr><><5 BucketID><6 /th><th style='text-align:right'>Total</th><th style='text-align:right; color:#28a745'>Opdateret</th><th style='text-align:right; farve:#dc3545 >ikke opdateret</th><><1 producent><2 /th></tr></thead><>"                         foreach ($b i $buckets) {                             $bid = if ($b.Name) { $b.Name } else { "(empty)" }                             $mfr = ($b.Group | Select-Object -First 1). WMI_Manufacturer                             # Hent antal opdaterede fra global bucket-statistik (alle enheder i denne bucket på tværs af hele datasættet)                             $lookupKey = $bid                             $globalBucket = if ($stAllBuckets.ContainsKey($lookupKey)) { $stAllBuckets[$lookupKey] } else { $null }                             $bUpdatedGlobal = if ($globalBucket) { $globalBucket.Updated } else { 0 }                             $bTotalGlobal = if ($globalBucket) { $globalBucket.Count } else { $b.Count }                             $bNotUpdatedGlobal = $bTotalGlobal - $bUpdatedGlobal                             $bucketSummary += "<tr><td style='font-size:.8em'>$bid><4 /td><td style='text-align:right; skrifttykkelse:bold'>$bTotalGlobal><8 /td><td style='text-align:right; farve:#28a745; skrifttykkelse:bold'>$bUpdatedGlobal><2 /td><td style='text-align:right; farve:#dc3545; skrifttykkelse:bold'>$bNotUpdatedGlobal><6 /td><td><9 $mfr</td></tr>'n"                         }                         $bucketSummary += "</tbody></table></div>"                     }                                          # ENHEDSDETALJER: Første N rækker som flad liste                     $slice = $data | Select-Object – første $MaxRows                     foreach ($d i $slice) {                         $conf = $d.ConfidenceLevel                         $confBadge = if ($conf -match "High") { '<span class="badge badge-success">High Conf><2 /span>' }                                      elseif ($conf -match "Not Sup") { '<span class="badge badge-danger">understøttes ikke><6 /span>' }                                      elseif ($conf -match "Under") { '<span class="badge badge-info">Under Obs><0 /span>' }                                      elseif ($conf -match "Midlertidigt afbrudt") { '<span class="badge badge-warning">Afbrudt midlertidigt><4 /span>' }                                      else { '<span class="badge-warning">Action Req><8 /span>' }                         $statusBadge = if ($d.IsUpdated) { '><00 span class="badge badge-success"><01 Opdateret</span>' }                                        elseif ($d.UEFICA2023Error) { '><04 span class="badge badge-danger"><05 Fejl</span>' }                                        else { '><08 span class="badge-warning"><09 Afventer><0 /span>' }                         $deviceRows += "><12 tr><td><5 $($d.HostName)><16 /td><td><9 $($d.WMI_Manufacturer)><20 /td><td><3 $($d.WMI_Model)><24 /td><td><7 $confBadge><8 /td><td><1 $statusBadge><2 /td><td><5 $(if($d.UEFICA2023Error){$d.UEFICA2023Error}else{'-'})><36 /td><td style='font-size:.75em'><39 $($d.BucketId)><40 /td></tr><3 'n"                     }                 } fange { }             }             if ($totalCount -eq 0) {                 return "><44 div style='padding:20px; farve:#888; font-style:kursiv'><45 Ingen enheder i denne kategori.><46 /div>"             }             $showing = [matematik]::Min($MaxRows, $totalCount)             $header = "><48 div style='margin:5px 0; font-size:.85em; color:#666'><49 Total: $($totalCount.ToString("N0")) devices"             if ($CsvFileName) { $header += " | ><50 a href='$CsvFileName' style='color:#1a237e; skrifttykkelse:bold'>&#128196; Download den fulde CSV-fil til Excel><3 /a>" }             $header += "><55 /div>"             $deviceHeader = "><57 h3 style='font-size:.95em; farve:#333; margin:10px 0 5px'><58 Enhedsoplysninger (viser første $showing)><59 /h3>"             $deviceTable = "><61 div style='max-height:500px; ><><tabel><><><><0 Værtsnavn><1><><4><5><><8 model><9><><2 konfidens><3><><><2><><6 status><7 /th><><0><1 /th><><4 BucketId><5 /th></tr></thead><tbody><2 $deviceRows><3 /tbody></table></div>"             return "$header$bucketSummary$deviceHeader$deviceTable"         }                  # Opbyg indbyggede tabeller fra JSON-filerne, der allerede findes på disken, og opret en kæde til CSV'er         $tblErrors = Build-InlineTable -JsonPath (joinsti $dataDir "errors.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_errors_$timestamp.csv"         $tblKI = Build-InlineTable -JsonPath (joinsti $dataDir "known_issues.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_known_issues_$timestamp.csv"         $tblKEK = Build-InlineTable -JsonPath (Join-Path $dataDir "missing_kek.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_missing_kek_$timestamp.csv"         $tblNotUpd = Build-InlineTable -JsonPath (joinsti $dataDir "not_updated.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_not_updated_$timestamp.csv"         $tblTaskDis = Build-InlineTable -JsonPath (Join-Path $dataDir "task_disabled.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_task_disabled_$timestamp.csv"         $tblTemp = Build-InlineTable -JsonPath (joinsti $dataDir "temp_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_temp_failures_$timestamp.csv"         $tblPerm = Build-InlineTable -JsonPath (joinsti $dataDir "perm_failures.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_perm_failures_$timestamp.csv"         $tblUpdated = Build-InlineTable -JsonPath (joinsti $dataDir "updated_devices.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_updated_devices_$timestamp.csv"         $tblActionReq = Build-InlineTable -JsonPath (joinsti $dataDir "action_required.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_action_required_$timestamp.csv"         $tblUnderObs = Build-InlineTable -JsonPath (joinsti $dataDir "under_observation.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_under_observation_$timestamp.csv"         $tblNeedsReboot = Build-InlineTable -JsonPath (joinsti $dataDir "needs_reboot.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_needs_reboot_$timestamp.csv"         $tblSBOff = Build-InlineTable -JsonPath (joinsti $dataDir "secureboot_off.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_secureboot_off_$timestamp.csv"         $tblRolloutIP = Build-InlineTable -JsonPath (joinsti $dataDir "rollout_inprogress.json") -MaxRows $maxInlineRows -CsvFileName "SecureBoot_rollout_inprogress_$timestamp.csv"         # Brugerdefineret tabel for Opdatering afventer – indeholder kolonnerne UEFICA2023Status og UEFICA2023Fejl         $tblUpdatePending = ""         $upJsonPath = Join-Path $dataDir "update_pending.json"         if ($upJsonPath teststi) {             prøv {                 $upData = Get-Content $upJsonPath -Raw | ConvertFrom-Json                 $upCount = $upData.Count                 if ($upCount -gt 0) {                     $upHeader = "<div style='margin:5px 0; font-size:.85em; color:#666'>Total: $($upCount.ToString("N0")) devices | <a href='SecureBoot_update_pending_$timestamp.csv' style='color:#1a237e; skrifttykkelse:bold'>&#128196; Download hele CSV-filen til Excel><4 /a></div>"                     $upRows = ""                     $upSlice = $upData | Select-Object – første $maxInlineRows                     foreach ($d i $upSlice) {                         $uefiSt = if ($d.UEFICA2023Status) { $d.UEFICA2023Status } else { '<span style="color:#999">null><0 /span>' }                         $uefiErr = if ($d.UEFICA2023Error) { "<span style='color:#dc3545'>$($d.UEFICA2023Error)</span>" } else { '-' }                         $policyVal = if ($d.AvailableUpdatesPolicy) { $d.AvailableUpdatesPolicy } else { '-' }                         $wincsVal = if ($d.WinCSKeyApplied) { '<span class="badge 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 = [matematik]::Min($maxInlineRows, $upCount)                     $upDevHeader = "<h3 style='font-size:.95em; farve:#333; margin:10px 0 5px'>Enhedsoplysninger (viser første $upShowing)</h3>"                     $upTable = "<div style='max-height:500px; overløb-y:auto'><tabel><><><><9 HostName><0><><3 Manufacturer><4 /th><><7 model><8 /th><><1 UEFICA2023Status><2 /th><><5 UEFICA2023Fejl><6 /th><><9 politik</th><>WinCS-tast</th><>BucketId</th></tr></thead><tbody><5 $upRows><6 /tbody></table></div>"                     $tblUpdatePending = "$upHeader$upDevHeader$upTable"                 } ellers {                     $tblUpdatePending = "<div style='padding:20px; farve:#888; skrifttype:kursiv'>Ingen enheder i denne kategori.</div>"                 }             } fange {                 $tblUpdatePending = "<div style='padding:20px; farve:#888; skrifttype:kursiv'>Ingen enheder i denne kategori.</div>"             }         } ellers {             $tblUpdatePending = "<div style='padding:20px; farve:#888; skrifttype:kursiv'>Ingen enheder i denne kategori.</div>"         }                  # Nedtælling for certifikatudløb         $certToday = Hent-dato         $certKekExpiry = [datetime]"2026-06-24"         $certUefiExpiry = [datetime]"2026-06-27"         $certPcaExpiry = [datetime]"2026-10-19"         $daysToKek = [matematik]::Maks.(0, ($certKekExpiry - $certToday). Dage)         $daysToUefi = [math]::Max(0, ($certUefiExpiry - $certToday). Dage)         $daysToPca = [matematik]::Maks.(0, ($certPcaExpiry - $certToday). Dage)         $certUrgency = if ($daysToKek -lt 30) { '#dc3545' } elseif ($daysToKek -lt 90) { '#fd7e14' } ellers { '#28a745' }                  # Build producent diagram data indbygget (Top 10 efter enhed antal)         $mfrSorted = $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Faldende | Select-Object -Første 10         $mfrChartTitle = if ($stMfrCounts.Count -le 10) { "Efter producent" } ellers { "Top 10 Producenter" }         $mfrLabels = ($mfrSorted | ForEach-Object { "'$($_. Nøgle)'" }) -join ","         $mfrUpdated = ($mfrSorted | ForEach-Object { $_. Value.Updated }) -join ","         $mfrUpdatePending = ($mfrSorted | ForEach-Object { $_. Value.UpdatePending }) -join ","         $mfrHighConf = ($mfrSorted | ForEach-Object { $_. Value.HighConf }) -join ","         $mfrUnderObs = ($mfrSorted | ForEach-Object { $_. Value.UnderObs }) -join ","         $mfrActionReq = ($mfrSorted | ForEach-Object { $_. Value.ActionReq }) -join ","         $mfrTempPaused = ($mfrSorted | ForEach-Object { $_. Value.TempPaused }) -join ","         $mfrNotSupported = ($mfrSorted | ForEach-Object { $_. Value.NotSupported }) -join ","         $mfrSBOff = ($mfrSorted | ForEach-Object { $_. Value.SBOff }) -join ","         $mfrWithErrors = ($mfrSorted | ForEach-Object { $_. Value.WithErrors }) -join ","                  # Build producent tabel         $mfrTableRows = ""         $stMfrCounts.GetEnumerator() | Sort-Object { $_. Value.Total } -Faldende | ForEach-Object {             $mfrTableRows += "<tr><td><7 $($_. Tast)</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 = @" <! DOCTYPE html-> <html lang="en"> <hoved><3 <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <titel><9 Dashboard til status for sikker bootstart><0 /title><1 <script src="https://cdn.jsdelivr.net/npm/chart.js"></script><5 <typografi><7 *{box-sizing:border-box; margen:0; udfyldning:0} body{font-family:'Segoe UI',Tahoma,sans-serif; baggrund:#f0f2f5; farve:#333} .header{background:linear-gradient(135deg,#1a237e,#0d47a1); farve:#fff; udfyldning:20px 30px} .header h1{font-size:1.6em; margen-bund:5px} .header .meta{font-size:.85em; uigennemsigtighed:.9} .container{max-width:1400px; margen:0 automatisk; udfyldning:20px} .cards{display:grid; gitterskabelon-kolonner:gentag(automatisk udfyldning,minmax(170px,1fr)); gap:12px; margin:20px 0} .card{background:#fff; kantradius:10px; udfyldning:15px; box-shadow:0 2px 8px rgba(0,0,0,.08); kant til venstre:4px massiv #ccc;overgang:transform .2s} .card:hover{transform:translateY(-2px); box-shadow:0 4px 15px rgba(0,0,0,.12)} .card .value{font-size:1.8em; skrifttykkelse:700} .card .label{font-size:.8em; farve:#666; margin-top:4px} .card .pct{font-size:.75em; farve:#888} .section{background:#fff; kantradius:10px; udfyldning:20px; margin:15px 0; box-shadow:0 2px 8px rgba(0,0,0,.08)} .section h2{font-size:1.2em; farve:#1a237e; margin-bottom:10px; markør:markør; brugervælg:none} .section h2:hover{text-decoration:understregning} .section-body{display:none} .section-body.open{display:block} .charts{display:grid; gitterskabelon-kolonner:1fr 1fr; gap:20px; margin:20px 0} .chart-box{background:#fff; kantradius:10px; udfyldning:20px; box-shadow:0 2px 8px rgba(0,0,0,.08)} tabel{width:100%; kant-skjul:skjule; skriftstørrelse:.85em} th{background:#e8eaf6; udfyldning:8px 10px; tekstjuster:venstre; position:klistret; top:0; z-index:1} td{padding:6px 10px; kant-bund:1px massiv #eee} tr:hover{background:#f5f5f5} .badge{display:inline-block; udfyldning:2px 8px;kantradius:10px; skriftstørrelse:.75em; skrifttykkelse:700} .badge-success{background:#d4edda; farve:#155724} .badge-danger{background:#f8d7da; farve:#721c24} .badge-warning{background:#fff3cd; farve:#856404} .badge-info{background:#d1ecf1; farve:#0c5460} .top-link{float:right; skriftstørrelse:.8em; farve:#1a237e; tekstdekoration:none} .footer{text-align:center; udfyldning:20px; farve:#999; skriftstørrelse:.8em} a{color:#1a237e} </style><9 </hoved> <brødtekst> <div class="header">     <h1>Statusdashboard til sikker bootstart</h1>     <div class="meta">genereret: $($stats. ReportGeneratedAt) | Enheder i alt: $($c.Total.ToString("N0")) | Entydige buckets: $($stAllBuckets.Count)</div><3 </div><5 <div class="container">

<!-- KPI-kort – klikbare, sammenkædet med sektioner – > <div class="cards">     <a class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#dc3545; tekstdekoration:ingen; position:relative"><div style="position:absolute; top:8px; right:8px; baggrund:#dc3545; farve:#fff; udfyldning:1px 6px; kantradius:8px; skriftstørrelse:.65em; font-weight:700">PRIMARY</div><div class="value" style="color:#dc3545">$($stNotUptodate.ToString(""N0"))</div><div class="label">IKKE OPDATERET><6 /div><div class="pct">$($stats. PercentNotUptodate)% – KRÆVER HANDLING><0 /div></a><3     <a class="card" href="#s-upd" onclick="openSection('d-upd')" style="border-left-color:#28a745; tekstdekoration:ingen; position:relative"><div style="position:absolute; top:8px; right:8px; baggrund:#28a745; farve:#fff; udfyldning:1px 6px; kantradius:8px; skriftstørrelse:.65em; font-weight:700">PRIMARY><8 /div><div class="value" style="color:#28a745">$($c.Updated.ToString("N0"))</div><div class="label">Opdateret><6 /div><div class="pct">$($stats. PercentCertUpdated)%</div></a><3     <a class="card" href="#s-sboff" onclick="openSection('d-sboff')" style="border-left-color:#6c757d; tekstdekoration:ingen; position:relative"><div style="position:absolute; top:8px; right:8px; baggrund:#6c757d; farve:#fff; udfyldning:1px 6px; kantradius:8px; skriftstørrelse:.65em; font-weight:700">PRIMARY><8 /div><div class="value"><1 $($c.SBOff.ToString("N0"))><2 /div><div class="label"><5 SecureBoot OFF</div><iv class="pct"><9 $(if($c.Total -gt 0){[math]::Round(($c.SBOff/$c.Total)*100;1)}else{0})% - Uden for området><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">skal genstartes><2 /div><div class ="pct">$(if($c.Total -gt 0){[math]::Round(($c.NeedsReboot/$c.Total)*100,1)}else{0})% - afventer genstart><6 /div></a><9     <a class="card" href="#s-upd-pend" onclick="openSection('d-upd-pend')" style="border-left-color:#6f42c1; text-decoration:none"><div class="value" style="color:#6f42c1">$($c.UpdatePending.ToString("N0"))</div><div class="label">Update Pending</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.UpdatePending/$c.Total)*100,1)}else{0})% - Politik/WinCS anvendt, afventer opdatering><2 /div></a><5     <en class="card" href="#s-rip" onclick="openSection('d-rip')" style="border-left-color:#17a2b8; text-decoration:none"><div class="value">$($c.RolloutInProgress)</div><div class="label">Rollout In Progress><4 /div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.RolloutInProgress/$c.Total)*100,1)}else{0})%</div></a><11     <a class="card" href="#s-nu" onclick="openSection('d-nu')" style="border-left-color:#28a745; text-decoration:none"><div class="value" style="color:#28a745">$($c.HighConf.ToString("N0"))</div><div class="label">High Confidence><20 /div><div class="pct">$($stats. PercentHighConfidence)% – Sikker til udrulning><24 /div></a><27     <a class="card" href="#s-uo" onclick="openSection('d-uo')" style="border-left-color:#17a2b8; text-decoration:none"><div class="value" style="color:#ffc107"><1 $($c.UnderObs.ToString("N0"))><2 /div><div class="label"><5 Under Observation><36 /div><div class="pct"><9 $(if($c.Total -gt 0){[math]::Round(($c.UnderObs/$c.Total)*100,1)}else{0})%</div></a><3     <a class="card" href="#s-ar" onclick="openSection('d-ar')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.ActionReq.ToString("N0"))</div><div class="label">Handling påkrævet><2 /div><div class="pct">$($stats. PercentActionRequired)% – skal teste><6 /div></a><9     <a class="card" href="#s-err" onclick="openSection('d-err')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($stAtRisk.ToString("N0"))</div><div class="label">At Risk><68 /div><div class="pct">$($stats. PercentAtRisk)% – Svarer til mislykkede><2 /div></a><5     <a class="card" href="#s-td" onclick="openSection('d-td')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.TaskDisabled.ToString("N0"))</div><div class="label">Opgave deaktiveret><4 /div><div class ="pct">$(if($c.Total -gt 0){[math]::Round(($c.TaskDisabled/$c.Total)*100;1)}else{0})% - Blokeret><8 /div></a><91     <a class="card" href="#s-tf" onclick="openSection('d-tf')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.TempPaused.ToString("N0"))</div><div class="label">Temp. Midlertidigt afbrudt</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TempPaused/$c.Total)*100,1)}else{0})%</div></a>     <a class="card" href="#s-ki" onclick="openSection('d-ki')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.WithKnownIssues.ToString("N0"))</div><div class="label">Kendte problemer><6 /div><d div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.WithKnownIssues/$c.Total)*100,1)}else{0})%</div></a><3     <a class="card" href="#s-kek" onclick="openSection('d-kek')" style="border-left-color:#fd7e14; text-decoration:none"><div class="value" style="color:#fd7e14">$($c.WithMissingKEK.ToString("N0"))</div><div class="label">Manglende KEK-</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.WithMissingKEK/$c.Total)*100;1)}else{0})%</div></a>     <a class="card" href="#s-err" onclick="openSection('d-err')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545">$($c.WithErrors.ToString("N0"))</div><div class="label">With Errors</div><div class="pct"><1 $($stats. PercentWithErrors)% – UEFI-fejl</div></a>     ><6 a class="card" href="#s-tf" onclick="openSection('d-tf')" style="border-left-color:#dc3545; text-decoration:none"><div class="value" style="color:#dc3545"><9 $($c.TempFailures.ToString("N0"))</div><div class="label">Temp. Fejl</div><div class="pct">$(if($c.Total -gt 0){[math]::Round(($c.TempFailures/$c.Total)*100,1)}else{0})%</div></a>     <a class="card" href="#s-pf" onclick="openSection('d-pf')" style="border-left-color:#721c24; text-decoration:none"><div class="value" style="color:#721c24">$($c.PermFailures.ToString("N0"))</div><div class="label">Ikke understøttet><6 /div><div class ="pct">$(if($c.Total -gt 0){[math]::Round(($c.PermFailures/$c.Total)*100;1)}else{0})%</div></a><3 </div>

<!-- udrulningshastighed & certifikatudløb --> <div id="s-velocity" style="display:grid; gitterskabelon-kolonner:1fr 1fr; gap:20px; margin:15px 0"> <div class="section" style="margin:0">     <h2>&#128197; Deployment Velocity</h2>     <div class="section-body open">         <div style="font-size:2.5em; skrifttykkelse:700; color:#28a745">$($c.Updated.ToString("N0"))</div>         <div style="color:#666">enheder opdateret ud af $($c.Total.ToString("N0"))</div>         <div style="margin:10px 0; baggrund:#e8eaf6; height:20px; kantradius:10px; overløb:skjult"><div style="background:#28a745; højde:100 %; bredde:$($stats. PercentCertUpdated)%; kantradius:10px"></div></div>         <div style="font-size:.8em; color:#888">$($stats. PercentCertUpdated)% fuldført</div>         <div style="margin-top:10px; udfyldning:10px; baggrund:#f8f9fa; kantradius:8px; skriftstørrelse:.85em">             <div><stærke>Resterende:</strong> $($stNotUptodate.ToString("N0"))-enheder kræver handling</div>             <div><stærk>Blokering:</strong> $($c.WithErrors + $c.PermFailures + $c.TaskDisabledNotUpdated)-enheder (fejl + permanent + opgave deaktiveret)</div>             <div><stærk>Sikker at installere:</strong> $($stSafeList.ToString("N0"))-enheder (samme bucket som fuldført)</div>             $velocityHtml         </div>     </div> </div> <div class="section" style="margin:0; kant mod venstre:4px udfyldt #dc3545">     <h2 style="color:#dc3545">&#9888; nedtælling for certifikatudløb</h2>     <div class="section-body open">         <div style="display:flex; gap:15px; margin-top:10px">             <div style="text-align:center; udfyldning:15px; kantradius:8px; min-width:120px; baggrund:lineær graduering(135deg;#fff5f5;#ffe0e0); border:2px solid #dc3545; flex:1">                 <div style="font-size:.65em; farve:#721c24; teksttransformation:store bogstaver; skrifttykkelse:bold">&#9888; FØRST TIL AT UDLØBE</div>                 ><4 div style="font-size:.85em; skrifttykkelse:fed; farve:#dc3545; margin:3px 0"><5 KEK CA 2011</div>                 ><8 div id="daysKek" style="font-size:2.5em; skrifttykkelse:700; farve:#dc3545; linjehøjde:1"><9 $daysToKek</div>                 ><2 div style="font-size:.8em; farve:#721c24"><3 dage (24. juni 2026)><4 /div>             ><6 /div>             ><8 div style="text-align:center; udfyldning:15px; kantradius:8px; min-width:120px; baggrund:lineær graduering(135deg,#fffef5,#fff3cd); border:2px solid #ffc107; flex:1"><9                 <div style="font-size:.65em; farve:#856404; teksttransformation:store bogstaver; font-weight:bold">UEFI CA 2011</div>                 <div id="daysUefi" style="font-size:2.2em; skrifttykkelse:700; farve:#856404; linjehøjde:1; margin:5px 0">$daysToUefi</div>                 <div style="font-size:.8em; farve:#856404">dage (27. juni 2026)</div>             </div>             <div style="text-align:center; udfyldning:15px; kantradius:8px; min-width:120px; baggrund:lineær graduering(135deg,#f0f8ff,#d4edff); border:2px solid #0078d4; flex:1">                 <div style="font-size:.65em; farve:#0078d4; teksttransformation:store bogstaver; font-weight:bold">Windows PCA</div>                 <div id="daysPca" style="font-size:2.2em; skrifttykkelse:700; farve:#0078d4; linjehøjde:1; margin:5px 0">$daysToPca><2 /div><3                 <div style="font-size:.8em; farve:#0078d4">dage (19. oktober 2026)</div><7             </div><9         </div><1         <div style="margin-top:15px; udfyldning:10px; baggrund:#f8d7da; kantradius:8px; font-size:.85em; kant mod venstre:4px udfyldt #dc3545">             <stærk>&#9888; KRITISK:</strong> Alle enheder skal opdateres, før certifikatet udløber. Enheder, der ikke opdateres inden deadline, kan ikke anvende fremtidige sikkerhedsopdateringer til Boot Manager og Sikker bootstart efter udløb.</div>     </div> </div> </div>

<!-- diagrammer – > <div class="charts">     <div class="chart-box"><h3>Installationsstatus</h3><canvas id="deployChart" height="200"></canvas></div><5     <div class="chart-box"><h3><9 $mfrChartTitle</h3><canvas id="mfrChart" height="200"></canvas></div> </div>

$(if ($historyData.Count -ge 1) { "<!-- historisk tendensdiagram – > <div class='section'>     <h2 onclick='"toggle('d-trend')'">&#128200; Opdater status over tid <en class='top-link' href='#'>&#8593; Øverste</a></h2>     <div id='d-trend' class='section-body open'>         <canvas id='trendChart' height='120'></canvas>         <div style='font-size:.75em; farve:#888; margin-top:5px'>Faste linjer = faktiske data$(if ($historyData.Count -ge 2) { " | Stiplet linje = projiceret (eksponentiel fordobling: 2&#x2192;4&#x2192;8&#x2192;16... enheder pr. bølge)" } ellers { " | Kør sammenlægning igen i morgen for at se tendenslinjer og projicering" })</div>     </div> </div>" })

<!-- CSV-overførsler – > <div class="section">     <h2 onclick="toggle('dl-csv')">&#128229; Download fuldstændige data (CSV til Excel) <a class="top-link" href="#">Øverste</a></h2><2     <div id="dl-csv" class="section-body open" style="display:flex; flex-wrap:wrap; mellemrum:5px">         <a href="SecureBoot_not_updated_$timestamp.csv" style="display:inline-block; baggrund:#dc3545; farve:#fff; udfyldning:6px 14px; kantradius:5px; tekstdekoration:ingen; font-size:.8em">Ikke opdateret ($($stNotUptodate.ToString("N0")))</a><8         <a href="SecureBoot_errors_$timestamp.csv" style="display:inline-block; baggrund:#dc3545; farve:#fff; udfyldning:6px 14px; kantradius:5px; tekstdekoration:ingen; font-size:.8em">Errors ($($c.WithErrors.ToString("N0")))</a>         <a href="SecureBoot_action_required_$timestamp.csv" style="display:inline-block; baggrund:#fd7e14; farve:#fff; udfyldning:6px 14px; kantradius:5px; tekstdekoration:ingen; font-size:.8em">Handling påkrævet ($($c.ActionReq.ToString("N0")))</a>         <a href="SecureBoot_known_issues_$timestamp.csv" style="display:inline-block; baggrund:#dc3545; farve:#fff; udfyldning:6px 14px; kantradius:5px; tekstdekoration:ingen; font-size:.8em">Kendte problemer ($($c.WithKnownIssues.ToString("N0")))</a>         <a href="SecureBoot_task_disabled_$timestamp.csv" style="display:inline-block; baggrund:#dc3545; farve:#fff; udfyldning:6px 14px; kantradius:5px; tekstdekoration:ingen; font-size:.8em">Opgave deaktiveret ($($c.TaskDisabled.ToString("N0")))</a>         <a href="SecureBoot_updated_devices_$timestamp.csv" style="display:inline-block; baggrund:#28a745; farve:#fff; udfyldning:6px 14px; kantradius:5px; tekstdekoration:ingen; font-size:.8em">Opdateret ($($c.Updated.ToString("N0")))</a>         <a href="SecureBoot_Summary_$timestamp.csv" style="display:inline-block; baggrund:#6c757d; farve:#fff; udfyldning:6px 14px; kantradius:5px; tekstdekoration:ingen; font-size:.8em">Oversigt</a>         <div style="width:100%; skriftstørrelse:.75em; farve:#888; margin-top:5px">CSV-filer åbne i Excel. Tilgængelig, når den hostes på webserveren.</div>     </div> </div>

<!-- Manufacturer Breakdown – > <div class="section">     <h2 onclick="toggle('mfr')">Af producent <a class="top-link" href="#">Top</a></h2><1     <div id="mfr" class="section-body open">     <tabel><de><><><1 fabrikant><2><><5><6 i alt><><9 opdateret><9><0><><3><4><den><7 handling, der kræves><8 /th></tr></thead><3     <><5 $mfrTableRows><6 /tbody></table><9     </div><1 </div>

<!-- enhedssektioner (første 200 indbyggede + CSV-download) --> <div class="section" id="s-err">     <h2 onclick="toggle('d-err')">&#128308; Enheder med fejl ($($c.WithErrors.ToString("N0"))) <a class="top-link" href="#">&#8593; Øverste</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; Kendte problemer ($($c.WithKnownIssues.ToString("N0"))) <a class="top-link" href="#">&#8593; Øverste</a></h2>     <div id="d-ki" class="section-body">$tblKI</div> </div> <div class="section" id="s-kek">     <h2 onclick="toggle('d-kek')">&#128992; Manglende KEK – Hændelse 1803 ($($c.WithMissingKEK.ToString("N0"))) <a class="top-link" href="#">&#8593; Top</a></h2>     >&#8593; 0 div id="d-kek" class="section-body">&#8593; 1 $tblKEK</div> >&#8593; 4 /div> >&#8593; 6 div class="section" id="s-ar">&#8593; 7     >&#8593; 8 h2 onclick="toggle('d-ar')" style="color:#fd7e14">&#128992; Handling påkrævet ($($c.ActionReq.ToString("N0"))) <a class="top-link" href="#">&#8593; Øverste><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; Under Observation ($($c.UnderObs.ToString("N0"))) <a class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-uo" class="section-body">$tblUnderObs</div> </div> <div class="section" id="s-nu">     <h2 onclick="toggle('d-nu')" style="color:#dc3545">&#128308; Ikke opdateret ($($stNotUptodate.ToString("N0"))) <a class="top-link" href="#">&#8593; Øverste</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; Opgave deaktiveret ($($c.TaskDisabled.ToString("N0"))) >&#8593; 5 a class="top-link" href="#">&#8593; Øverste</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; Midlertidige fejl ($($c.TempFailures.ToString("N0"))) <a class="top-link" href="#">&#8593; Øverste</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; Permanente fejl / Understøttes ikke ($($c.PermFailures.ToString("N0"))) <a class="top-link" href="#">&#8593; Øverste</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; Opdatering venter ($($c.UpdatePending.ToString("N0"))) - Politik/WinCS anvendt, Afventer opdatering <en class="top-link" href="#">&#8593; Øverste</a></h2>     <div id="d-upd-pend" class="section-body"><p style="color:#666; margin-bottom:10px">Enheder, hvor availableUpdatesPolicy- eller WinCS-tasten anvendes, men UEFICA2023Status stadig er NotStarted, InProgress eller null.</p>$tblUpdatePending</div> </div> <div class="section" id="s-rip">     <h2 onclick="toggle('d-rip')" style="color:#17a2b8">&#128309; Igangværende udrulning ($($c.RolloutInProgress.ToString("N0"))) <a class="top-link" href="#">&#8593; Øverste</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"))) - Uden for området <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; Opdaterede enheder ($($c.Updated.ToString("N0"))) <a class="top-link" href="#">&#8593; Top</a></h2>     <div id="d-upd" class="section-body">$tblUpdated</div> </div> <div class="section" id="s-nrb">     <h2 onclick="toggle('d-nrb')" style="color:#ffc107">&#128260; Opdateret – Kræver genstart ($($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">Secure Boot Certificate Rollout Dashboard | Genererede $($stats. ReportGeneratedAt) | StreamingMode | Spidsbelastningshukommelse: ${stPeakMemMB} MB</div> </div><!-- /container -->

<script> funktions toggle(id){var e=document.getElementById(id); e.classList.toggle('open')} funktionen openSection(id){var e=document.getElementById(id); if(e&&!e.classList.contains('open')){e.classList.add('open')}} nyt diagram(document.getElementById('deployChart'),{type:'kranse',data:{labels:['Updated','Update Pending','High Confidence','Under Observation','Action Required','Temp. Midlertidigt afbrudt','ikke understøttet','SecureBoot OFF','With Errors'],datasæt:[{data:[$($c.Updated),$($c.UpdatePending),$($c.HighConf),$($c.UnderObs),$($c.ActionReq),$($c.TempPaused),$($c.NotSupported),$($c.SBOff),$($c.WithErrors)],backgroundColor:['#28a745','#6f42c1','#20c997','#17a2b8','#fd7e14','#6c757d','#721c24 #17a2b8','#adb5bd','#dc3545']}]},options:{responsive:true,plugins:{legend:{position:'right',labels:{font:{size:11}}}}}}); nyt diagram(document.getElementById('mfrChart'),{type:'bar',data:{labels:[$mfrLabels],datasæt:[{label:'Updated',data:[$mfrUpdated],backgroundColor:'#28a745'},{label:'Update Pending',data:[$mfrUpdatePending],backgroundColor:'#6f42c1'},{label:'High Confidence',data:[$mfrHighConf],backgroundColor:'#20c997'},{label:'Under Observation', data:[$mfrUnderObs],backgroundColor:'#17a2b8'},{label:'Action Required',data:[$mfrActionReq],backgroundColor:'#fd7e14'},{ label:'Temp. Midlertidigt afbrudt',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'}}}}); Historisk tendensdiagram 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); } var datasæt = [     {label:'Updated',data:paddedUpdated,borderColor:'#28a745',backgroundColor:'rgba(40,167,69,0.1)',fill:true,tension:0.3,borderWidth:2},     {label:'Not Updated',data:paddedNotUpdated,borderColor:'#dc3545',backgroundColor:'rgba(220,53,69,0.1)',fill:true,tension:0.3,borderWidth:2},     {label:'Total',data:paddedTotal,borderColor:'#6c757d',borderDash:[5,5],fill:false,tension:0,pointRadius:0,borderWidth:1} ]; if (projLen > 0) {     datasets.push({label:'Projected Updated (2x doubling)',data:projLine,borderColor:'#28a745',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'});     datasets.push({label:'Projected Not Updated',data:projNotUpdLine,borderColor:'#dc3545',borderDash:[8,4],borderWidth:3,fill:false,tension:0.3,pointRadius:3,pointStyle:'triangle'}); } nyt diagram(document.getElementById('trendChart'),{type:'line',data:{labels:allLabels,datasæt:datasets},options:{responsive:true,scales:{y:{beginAtZero:true,title:{display:{display: true,text:'Devices'}},x:{title:{display:true,text:'Date'}}},plugins:{legend:{position:'top'},title:{display:true,text:'Secure Boot Update Progress Over Time'}}}}); } Dynamisk nedtælling (funktion(){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> </body> </html-> "@         [System.IO.File]::WriteAllText($htmlPath, $htmlContent, [System.Text.UTF8Encoding]:new($false))         # Hold altid en stabil "Seneste" kopi, så administratorer ikke behøver at spore tidsstempler         $latestPath = Join-Path $OutputPath "SecureBoot_Dashboard_Latest.html"         Copy-Item $htmlPath $latestPath -Force         $stTotal = $streamSw.Forløbet.TotalSeconds         # Gem filmanifest for trinvis tilstand (hurtig registrering af ændringer uden ændringer ved næste kørsel)         if ($IncrementalMode -or $StreamingMode) {             $stManifestDir = Join-Path $OutputPath ".cache"             if (-not (Test-Path $stManifestDir)) { New-Item -ItemType Directory -Path $stManifestDir -Force | Out-Null }             $stManifestPath = Join-Path $stManifestDir "StreamingManifest.json"             $stNewManifest = @{}             Write-Host "Gemmer filmanifest for trinvis tilstand..." -Forgrundsfarve Grå             foreach ($jf i $jsonFiles) {                 $stNewManifest[$jf. FullName.ToLowerInvariant()] = @{                     LastWriteTimeUtc = $jf. LastWriteTimeUtc.ToString("o")                     Størrelse = $jf. Længde                 }             }             Save-FileManifest -Manifest $stNewManifest -Path $stManifestPath             Write-Host " Gemt manifest for $($stNewManifest.Count)-filer" -Forgrundsfarve DarkGray         }         # OPBEVARINGSOPRYDNING         # Orchestrator-mappe, der kan genbruges (Aggregation_Current): Behold kun seneste kørsel (1)         # Administration manuel kører / andre mapper: hold sidste 7 kører         # Summary CSV'er slettes ALDRIG – de er små (~1 KB) og er backupkilden til tendenshistorikken         $outputLeaf = Split-Path $OutputPath -Blad         $retentionCount = hvis ($outputLeaf -eq 'Aggregation_Current') { 1 } ellers { 7 }         # Filpræfikser, der er sikre at rydde op i (midlertidige øjebliksbilleder pr. kørsel)         $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_'         )         # Find kun alle unikke tidsstempler fra filer, der kan ryddes         $cleanableFiles = Get-ChildItem $OutputPath -File -EA SilentlyContinue |             Where-Object { $f = $_. Navn; ($cleanupPrefixes | Where-Object { $f.StartsWith($_) }). Tæl -gt 0 }         $allTimestamps = @($cleanableFiles | ForEach-Object {             if ($_. Name -match '(\d{8}-\d{6})') { $Matches[1] }         } | Sort-Object -Entydig -faldende)         if ($allTimestamps.Count -gt $retentionCount) {             $oldTimestamps = $allTimestamps | Select-Object – Spring $retentionCount over             $removedFiles = 0; $freedBytes = 0             foreach ($oldTs i $oldTimestamps) {                 foreach ($prefix i $cleanupPrefixes) {                     $oldFiles = Get-ChildItem $OutputPath -File -Filter "${prefix}${oldTs}*" -EA SilentlyContinue                     foreach ($f i $oldFiles) {                         $freedBytes += $f.Length                         Remove-Item $f.FullName -Force -EA SilentlyContinue                         $removedFiles++                     }                 }             }             $freedMB = [matematik]::Rund($freedBytes / 1 MB, 1)             Write-Host "Opbevaringsoprydning: fjernede $removedFiles filer fra gamle køreversioner af $($oldTimestamps.Count), frigjort ${freedMB} MB (holder sidste $retentionCount + alle Oversigts-/NotUptodate CSVs)" -ForegroundColor DarkGray         }         Write-Host "'n$("=" * 60)" -Forgrundsfarve cyan         Write-Host "STREAMING AGGREGATION COMPLETE" -Forgrundsfarve grøn         Write-Host ("=" * 60) -Forgrundsfarve cyan         Write-Host " Enheder i alt: $($c.Total.ToString("N0"))" -Forgrundsfarve hvid         Write-Host " IKKE OPDATERET: $($stNotUptodate.ToString("N0")) ($($stats. PercentNotUptodate)%)" -Forgrundsfarve $(if ($stNotUptodate -gt 0) { "Gul" } ellers { "Grøn" })         Write-Host " Opdateret: $($c.Updated.ToString("N0")) ($($stats. PercentCertUpdated)%)" -Forgrundsfarve Grøn         Write-Host " With Errors: $($c.WithErrors.ToString("N0"))" -ForegroundColor $(if ($c.WithErrors -gt 0) { "Red" } else { "Green" })         Write-Host " Peak Memory: ${stPeakMemMB} MB" -ForegroundColor Cyan         Write-Host " Klokkeslæt: $([matematik]::Rund($stTotal/60,1)) min" -Forgrundsfarve hvid         Write-Host " Dashboard: $htmlPath" -Forgrundsfarve hvid         return [PSCustomObject]$stats     }     #endregion STREAMINGTILSTAND } ellers {     Write-Error "Inputsti blev ikke fundet: $InputPath"     afslut 1 }                                                      

Har du brug for mere hjælp?

Vil du have flere indstillinger?

Udforsk abonnementsfordele, gennemse kurser, få mere at vide om, hvordan du sikrer din enhed og meget mere.