<# : Startteil fuer die Eingabeaufforderung (cmd) - der eigentliche Code steht weiter unten (PowerShell) @echo off setlocal rem Programme nur aus Windows starten, nie aus dem Ordner dieser Datei (z. B. Downloads) set "NoDefaultCurrentDirectoryInExePath=1" set "PCP_DATEI=%~f0" set "PCP_PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" "%SystemRoot%\System32\net.exe" session >nul 2>&1 if errorlevel 1 ( echo PC-Pflege braucht Administratorrechte. Windows fragt gleich nach. "%PCP_PS%" -NoProfile -Command "Start-Process -FilePath $env:PCP_DATEI -Verb RunAs" exit /b ) "%PCP_PS%" -NoProfile -ExecutionPolicy Bypass -Command "& ([ScriptBlock]::Create([IO.File]::ReadAllText($env:PCP_DATEI, [Text.Encoding]::UTF8)))" exit /b #> # ===================================================================================================== # PC-Pflege für Windows 11 · Version 1.0.0 · https://pcpflege.geofox-it.de # © 2026 Janek Voß · Lizenz: MIT (Text am Ende dieser Datei) · Nutzung auf eigene Gefahr # # Teil 1 prüft den PC (nur lesend). Teil 2 pflegt ihn mit Bordmitteln von Windows. # Das Ergebnis steht in einem Bericht (PDF, erzeugt mit Microsoft Edge; ohne Edge als HTML-Datei), # das Fenster zeigt nur den Fortschritt und die Fragen. # Vor allem, was etwas löscht, installiert oder lange dauert, wird vorher gefragt. # Installiert nichts, legt keine Dienste oder geplanten Aufgaben an, sendet keine Daten. # Ablage: %ProgramData%\PC-Pflege (Berichte, Protokolle, Stand des letzten Laufs, Laufwerks-Messwerte, Sicherungen) # # Start: Doppelklick (Windows fragt nach Administratorrechten) oder Rechtsklick > Als Administrator ausführen. # Hinweis für Bearbeiter: keine typografischen Anführungszeichen im Code (PowerShell hält sie für echte), # im Text »…« verwenden. Datei als UTF-8 ohne BOM mit Windows-Zeilenenden speichern. # ===================================================================================================== $Version = '1.0.0' $ErrorActionPreference = 'SilentlyContinue' try { $Host.UI.RawUI.WindowTitle = "PC-Pflege $Version" } catch {} $ich = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() if (-not $ich.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Host 'PC-Pflege braucht Administratorrechte: Rechtsklick auf die Datei > Als Administrator ausführen.' -ForegroundColor Yellow Read-Host 'Enter drücken zum Schließen' | Out-Null return } $Ablage = Join-Path $env:ProgramData 'PC-Pflege' $LogOrdner = Join-Path $Ablage 'Protokolle' $BerichtOrdner = Join-Path $Ablage 'Berichte' $SicherOrdner = Join-Path $Ablage 'Sicherungen' $StandDatei = Join-Path $Ablage 'letzter-lauf.json' $MessDatei = Join-Path $Ablage 'laufwerke-verlauf.csv' # Ablage absichern: Unter ProgramData dürften normale Benutzer sonst Ordner anlegen - etwa eine Verknüpfung (Junction) # auf einen Systemordner, in dem die Pflege mit Administratorrechten dann schreiben oder löschen würde. # Deshalb: Schreiben nur für Administratoren und SYSTEM, alle anderen dürfen lesen (für die Berichte). # Ist die Ablage oder ein Unterordner trotzdem eine Verknüpfung, bricht die PC-Pflege ab. New-Item -ItemType Directory -Force $Ablage | Out-Null $rechte = New-Object Security.AccessControl.DirectorySecurity $rechte.SetAccessRuleProtection($true, $false) foreach ($sid in 'S-1-5-32-544', 'S-1-5-18') { # Administratoren, SYSTEM $rechte.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule((New-Object Security.Principal.SecurityIdentifier $sid), 'FullControl', 'ContainerInherit, ObjectInherit', 'None', 'Allow'))) } $rechte.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule((New-Object Security.Principal.SecurityIdentifier 'S-1-5-32-545'), 'ReadAndExecute', 'ContainerInherit, ObjectInherit', 'None', 'Allow'))) # Benutzer: lesen if (-not ((Get-Item -LiteralPath $Ablage -Force).Attributes -band [IO.FileAttributes]::ReparsePoint)) { Set-Acl -LiteralPath $Ablage -AclObject $rechte } New-Item -ItemType Directory -Force $LogOrdner, $BerichtOrdner | Out-Null foreach ($o in $Ablage, $LogOrdner, $BerichtOrdner, $SicherOrdner) { if ((Test-Path -LiteralPath $o) -and ((Get-Item -LiteralPath $o -Force).Attributes -band [IO.FileAttributes]::ReparsePoint)) { Write-Host "Abbruch: $o ist eine Verknüpfung auf einen anderen Ordner. Bitte prüfen und den Ordner löschen." -ForegroundColor Red Read-Host 'Enter drücken zum Schließen' | Out-Null return } } $Start = Get-Date $LogDatei = Join-Path $LogOrdner ('PC-Pflege-{0:yyyy-MM-dd_HHmm}.log' -f $Start) $BerichtName = 'PC-Pflege-Bericht-{0:yyyy-MM-dd_HHmm}' -f $Start Start-Transcript -Path $LogDatei | Out-Null $LW = $env:SystemDrive.TrimEnd(':') # Systemlaufwerk, fast immer C # ---------------------------------------------------------------------------------------------------- # Hilfsfunktionen. Während der Pflege landet jede Meldung zusätzlich im Bericht ($Protokoll). # ---------------------------------------------------------------------------------------------------- $Protokoll = New-Object System.Collections.Generic.List[object] $Sammeln = $false function Merke($art, $t) { if ($script:Sammeln) { $script:Protokoll.Add([pscustomobject]@{ Art = $art; Text = "$t" }) } } function Schritt($t) { Write-Host ''; Write-Host "=== $t ===" -ForegroundColor Cyan; Merke 'schritt' $t } function Ok($t) { Write-Host " OK: $t" -ForegroundColor Green; Merke 'ok' $t } function Info($t) { Write-Host " $t"; Merke 'info' $t } function Warn($t) { Write-Host " $t" -ForegroundColor Yellow; Merke 'warn' $t } function Fortschritt($t) { Write-Host " · $t" -ForegroundColor DarkGray } function Frage($q) { $a = Read-Host " $q [j/n]"; $ja = ($a -match '^\s*(j|ja|y|yes)\s*$'); Merke 'frage' ("$q - " + $(if ($ja) { 'ja' } else { 'nein' })); return $ja } function D($d) { if ($d) { ([datetime]$d).ToString('dd.MM.yyyy HH:mm') } else { 'nie' } } function GB($p) { if (-not $p -or -not (Test-Path -LiteralPath $p)) { return 0 } $s = (Get-ChildItem -LiteralPath $p -Recurse -Force -File -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum return [math]::Round($s / 1GB, 2) } # Löschen, ohne Ordner-Verknüpfungen (Junctions, symbolische Links) zu folgen: Remove-Item -Recurse in Windows PowerShell 5.1 # würde in den Zielordner hineinlöschen. Ein normaler Benutzer könnte so in C:\Windows\Temp eine Verknüpfung auf einen # Systemordner legen. Verknüpfungen werden hier nur selbst entfernt; .NET löscht Ordner, ohne Verknüpfungen zu folgen. function SicherLoeschen($e) { try { if ($e.Attributes -band [IO.FileAttributes]::ReparsePoint) { if ($e -is [IO.DirectoryInfo]) { [IO.Directory]::Delete($e.FullName, $false) } else { [IO.File]::Delete($e.FullName) } } elseif ($e -is [IO.DirectoryInfo]) { [IO.Directory]::Delete($e.FullName, $true) } else { if ($e.IsReadOnly) { $e.IsReadOnly = $false }; [IO.File]::Delete($e.FullName) } } catch {} # gesperrte Dateien laufender Programme bleiben einfach liegen } function AlteLoeschen($pfad, $tage) { if (-not (Test-Path -LiteralPath $pfad)) { return } $grenze = (Get-Date).AddDays(-$tage) foreach ($e in @(Get-ChildItem -LiteralPath $pfad -Force -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt $grenze })) { SicherLoeschen $e } } function PapierkorbGB { $s = 0 foreach ($v in (Get-Volume | Where-Object { $_.DriveLetter -and $_.DriveType -eq 'Fixed' })) { $s += GB "$($v.DriveLetter):\`$Recycle.Bin" } return $s } function NeustartAussteht { (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') -or (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') } # ---------------------------------------------------------------------------------------------------- # Reste deinstallierter Programme # App-Eintrag : Eintrag in »Installierte Apps«, dessen MSI-Paket nicht mehr registriert ist # oder dessen Deinstallationsprogramm bzw. Programmordner fehlt # Dienst : Windows-Dienst, dessen Programmdatei fehlt # Aufgabe : geplante Aufgabe (außer \Microsoft\), deren Programmdatei fehlt # Verknüpfung : Verknüpfung im Startmenü oder auf dem Desktop, deren Ziel fehlt # Vor dem Entfernen wird jeder Rest in %ProgramData%\PC-Pflege\Sicherungen gesichert. # ---------------------------------------------------------------------------------------------------- function Get-ExePfad([string]$cmd) { if (-not $cmd) { return $null } $c = [Environment]::ExpandEnvironmentVariables($cmd.Trim()) $c = $c -replace '^\\SystemRoot\\', ($env:SystemRoot + '\') -replace '^\\\?\?\\', '' if ($c -match '^"([^"]+)"') { $p = $matches[1] } elseif ($c -match '^(.+?\.(exe|cmd|bat|com))(\s|"|$)') { $p = $matches[1] } else { return $null } if ($p -match '^(system32|syswow64)\\') { $p = Join-Path $env:SystemRoot $p } if (-not [IO.Path]::IsPathRooted($p)) { return $null } return $p } function Get-MsiStatus([string]$guid) { # 5 = installiert, 1 = angekündigt, 2 = fehlt, -1 = unbekannt try { $inst = New-Object -ComObject WindowsInstaller.Installer return [int]$inst.GetType().InvokeMember('ProductState', 'GetProperty', $null, $inst, @($guid)) } catch { return 5 } # im Zweifel als installiert werten } function Find-Reste { $reste = New-Object System.Collections.Generic.List[object] $wurzeln = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall', 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' foreach ($w in $wurzeln) { foreach ($key in (Get-ChildItem $w -ErrorAction SilentlyContinue)) { $e = Get-ItemProperty $key.PSPath -ErrorAction SilentlyContinue if (-not $e.DisplayName -or $e.SystemComponent -eq 1 -or $e.ParentKeyName) { continue } $grund = $null $guid = if ($key.PSChildName -match '^\{[0-9A-Fa-f-]{36}\}$') { $key.PSChildName } elseif ($e.UninstallString -match '(\{[0-9A-Fa-f-]{36}\})' -and $e.UninstallString -match 'msiexec') { $matches[1] } if ($e.WindowsInstaller -eq 1 -or ($guid -and $e.UninstallString -match 'msiexec')) { if ($guid -and (Get-MsiStatus $guid) -in -1, 2) { $grund = 'MSI-Paket nicht mehr registriert' } } else { $exe = Get-ExePfad $e.UninstallString $ort = if ($e.InstallLocation) { $e.InstallLocation.Trim().Trim('"') } else { $null } if ($exe -and -not (Test-Path -LiteralPath $exe)) { $grund = 'Deinstallationsprogramm fehlt' } elseif ($ort -and [IO.Path]::IsPathRooted($ort) -and -not (Test-Path -LiteralPath $ort)) { $grund = 'Programmordner fehlt' } } if ($grund) { $reste.Add([pscustomobject]@{ Art = 'App-Eintrag'; Name = "$($e.DisplayName) $($e.DisplayVersion)".Trim(); Grund = $grund; Ziel = $key.Name }) } } } foreach ($s in (Get-CimInstance Win32_Service -ErrorAction SilentlyContinue)) { $exe = Get-ExePfad $s.PathName if ($exe -and -not (Test-Path -LiteralPath $exe)) { $reste.Add([pscustomobject]@{ Art = 'Dienst'; Name = "$($s.DisplayName) ($($s.Name))"; Grund = "Programmdatei fehlt: $exe"; Ziel = $s.Name }) } } foreach ($t in (Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskPath -notlike '\Microsoft\*' })) { foreach ($a in @($t.Actions | Where-Object { $_.Execute })) { $exe = Get-ExePfad ('"' + $a.Execute.Trim('"') + '"') if ($exe -and -not (Test-Path -LiteralPath $exe)) { $reste.Add([pscustomobject]@{ Art = 'Aufgabe'; Name = "$($t.TaskPath)$($t.TaskName)"; Grund = "Programmdatei fehlt: $exe"; Ziel = "$($t.TaskPath)|$($t.TaskName)" }) break } } } $sh = New-Object -ComObject WScript.Shell $orte = "$env:ProgramData\Microsoft\Windows\Start Menu\Programs", "$env:APPDATA\Microsoft\Windows\Start Menu\Programs", [Environment]::GetFolderPath('Desktop'), "$env:PUBLIC\Desktop" foreach ($o in $orte) { foreach ($l in (Get-ChildItem -LiteralPath $o -Filter *.lnk -Recurse -Force -ErrorAction SilentlyContinue)) { try { $ziel = $sh.CreateShortcut($l.FullName).TargetPath } catch { continue } if ($ziel -and [IO.Path]::IsPathRooted($ziel) -and $ziel -notmatch '^\\\\' -and -not (Test-Path -LiteralPath $ziel)) { $reste.Add([pscustomobject]@{ Art = 'Verknüpfung'; Name = $l.FullName; Grund = "Ziel fehlt: $ziel"; Ziel = $l.FullName }) } } } return $reste } function Remove-Rest($r, [string]$sicherung) { New-Item -ItemType Directory -Force $sicherung | Out-Null $datei = Join-Path $sicherung (($r.Art + '_' + $r.Name) -replace '[\\/:*?"<>|\s]+', '_') switch ($r.Art) { 'App-Eintrag' { & reg.exe export $r.Ziel "$datei.reg" /y | Out-Null Remove-Item -LiteralPath ('Registry::' + $r.Ziel) -Recurse -Force -ErrorAction Stop } 'Dienst' { & reg.exe export "HKLM\SYSTEM\CurrentControlSet\Services\$($r.Ziel)" "$datei.reg" /y | Out-Null Stop-Service -Name $r.Ziel -Force -ErrorAction SilentlyContinue & sc.exe delete $r.Ziel | Out-Null if ($LASTEXITCODE -ne 0) { throw "sc delete meldet $LASTEXITCODE" } } 'Aufgabe' { $pfad, $name = $r.Ziel -split '\|', 2 Export-ScheduledTask -TaskPath $pfad -TaskName $name | Set-Content "$datei.xml" -Encoding Unicode Unregister-ScheduledTask -TaskPath $pfad -TaskName $name -Confirm:$false -ErrorAction Stop } 'Verknüpfung' { Copy-Item -LiteralPath $r.Ziel "$datei.lnk" -Force Remove-Item -LiteralPath $r.Ziel -Force -ErrorAction Stop $ordner = Split-Path $r.Ziel if ($ordner -like '*\Start Menu\Programs\*' -and -not (Get-ChildItem -LiteralPath $ordner -Force -ErrorAction SilentlyContinue)) { Remove-Item -LiteralPath $ordner -Force -ErrorAction SilentlyContinue } } } } # ---------------------------------------------------------------------------------------------------- # Geräte-Manager: Bedeutung der häufigsten Fehlercodes # ---------------------------------------------------------------------------------------------------- $GeraeteCodes = @{ 1 = 'nicht richtig eingerichtet'; 3 = 'Treiber beschädigt oder zu wenig Arbeitsspeicher'; 10 = 'kann nicht gestartet werden' 12 = 'nicht genug freie Ressourcen'; 14 = 'braucht einen Neustart'; 18 = 'Treiber neu installieren'; 19 = 'Einstellungen in der Registry beschädigt' 24 = 'nicht richtig erkannt'; 28 = 'kein Treiber installiert'; 31 = 'Treiber konnte nicht geladen werden'; 32 = 'Treiber ist deaktiviert' 39 = 'Treiber fehlt oder ist beschädigt'; 43 = 'Gerät hat einen Fehler gemeldet'; 48 = 'Treiber wurde blockiert'; 52 = 'Signatur des Treibers ungültig' } # ==================================================================================================== # TEIL 1: PRÜFEN (nur lesend) # ==================================================================================================== Clear-Host Write-Host "PC-Pflege $Version" -ForegroundColor Cyan Write-Host 'Teil 1: Der PC wird geprüft. Das dauert einige Minuten, es wird nichts verändert.' Write-Host 'Das Ergebnis erscheint danach als Bericht.' Write-Host '' $letzter = $null if (Test-Path $StandDatei) { $letzter = Get-Content $StandDatei -Raw -Encoding UTF8 | ConvertFrom-Json } # --- Windows --- Fortschritt 'Windows und Updates' $os = Get-CimInstance Win32_OperatingSystem $nt = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' $windows = "$($os.Caption) $($nt.DisplayVersion) (Build $($nt.CurrentBuild).$($nt.UBR))" $pcName = $env:COMPUTERNAME # Letztes kumulatives Update aus dem Windows-Update-Verlauf (Virensignaturen zählen nicht), sonst aus der Hotfix-Liste $letztesUpdate = $null try { $us = (New-Object -ComObject Microsoft.Update.Session).CreateUpdateSearcher() $n = $us.GetTotalHistoryCount() if ($n -gt 0) { $treffer = @($us.QueryHistory(0, [math]::Min($n, 200)) | Where-Object { $_.Operation -eq 1 -and $_.ResultCode -in 2, 3 -and $_.Title -match '(?i)kumulativ|cumulative' } | Sort-Object Date -Descending | Select-Object -First 1) if ($treffer.Count) { $letztesUpdate = $treffer[0].Date.ToLocalTime() } } } catch {} if (-not $letztesUpdate) { $letztesUpdate = (Get-HotFix | Where-Object InstalledOn | Sort-Object InstalledOn -Descending | Select-Object -First 1).InstalledOn } $neustartAus = NeustartAussteht # Reste früherer Windows-Versionen (nach großen Funktionsupdates). In den ersten 10 Tagen braucht Windows sie, # um zur vorherigen Version zurückkehren zu können - erst danach werden sie als überflüssig gemeldet. $altWinOrdner = @("$($LW):\Windows.old", "$($LW):\`$Windows.~BT", "$($LW):\`$Windows.~WS" | Where-Object { Test-Path -LiteralPath $_ }) $altWinGB = 0; foreach ($o in $altWinOrdner) { $altWinGB += GB $o } $altWinAlter = if ($altWinOrdner.Count) { ((Get-Date) - (Get-Item -LiteralPath $altWinOrdner[0] -Force).CreationTime).Days } else { 0 } # --- Speicher --- Fortschritt 'Speicher' $vol = Get-Volume -DriveLetter $LW $freiGB = [math]::Round($vol.SizeRemaining / 1GB, 1) $gesamtGB = [math]::Round($vol.Size / 1GB, 1) $freiPct = if ($vol.Size) { $vol.SizeRemaining / $vol.Size * 100 } else { 100 } $papierkorb = PapierkorbGB $tempGB = (GB $env:TEMP) + (GB "$env:SystemRoot\Temp") $SpPfad = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\StorageSense\Parameters\StoragePolicy' $speicheroptimierung = ((Get-ItemProperty $SpPfad).'01' -eq 1) $andereLW = @(Get-Volume | Where-Object { $_.DriveLetter -and $_.DriveType -eq 'Fixed' -and $_.DriveLetter -ne $LW -and $_.Size -gt 0 }) # Speicherfresser im Benutzerordner: größte Ordner (AppData eine Ebene tiefer) und Dateien ab 1 GB. # Dateien, die nur in OneDrive liegen (nicht heruntergeladen), belegen keinen Platz und zählen nicht mit. Fortschritt 'Speicherfresser im Benutzerordner (kann einige Minuten dauern)' # Ordner-Verknüpfungen (Junctions) werden nicht verfolgt - sonst drohen Endlosschleifen. Ausnahme: OneDrive-Ordner, # die mit »Dateien bei Bedarf« ebenfalls als Verknüpfung markiert sind. $profil = $env:USERPROFILE $oneDrive = @($env:OneDrive, $env:OneDriveConsumer, $env:OneDriveCommercial | Where-Object { $_ } | Sort-Object -Unique) $ordnerGroesse = @{} $grosseDateien = New-Object System.Collections.Generic.List[object] $stapel = New-Object System.Collections.Generic.Stack[IO.DirectoryInfo] $stapel.Push([IO.DirectoryInfo]$profil) while ($stapel.Count) { $ordner = $stapel.Pop() try { $eintraege = $ordner.GetFileSystemInfos() } catch { continue } foreach ($e in $eintraege) { if ($e -is [IO.DirectoryInfo]) { $istLink = [bool]($e.Attributes -band [IO.FileAttributes]::ReparsePoint) if (-not $istLink -or ($oneDrive | Where-Object { $e.FullName -like "$_*" })) { $stapel.Push($e) } continue } if ([int]$e.Attributes -band 0x401000) { continue } # nur online gespeichert (OneDrive) - belegt keinen Platz $teile = $e.FullName.Substring($profil.Length).TrimStart('\').Split('\') $schluessel = if ($teile.Count -lt 2) { '(direkt im Benutzerordner)' } elseif ($teile[0] -eq 'AppData' -and $teile.Count -ge 4) { "AppData\$($teile[1])\$($teile[2])" } else { $teile[0] } $ordnerGroesse[$schluessel] += $e.Length if ($e.Length -ge 1GB) { $grosseDateien.Add([pscustomobject]@{ Pfad = $e.FullName; GB = [math]::Round($e.Length / 1GB, 1); Datum = $e.LastWriteTime }) } } } $topOrdner = @($ordnerGroesse.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 10 | ForEach-Object { [pscustomobject]@{ Ordner = $_.Key; GB = [math]::Round($_.Value / 1GB, 1) } }) $topDateien = @($grosseDateien | Sort-Object GB -Descending | Select-Object -First 10) $downloadsGB = [math]::Round(($ordnerGroesse['Downloads'] + 0) / 1GB, 1) # --- Sicherheit --- Fortschritt 'Sicherheit' $fremdAV = @(Get-CimInstance -Namespace 'root/SecurityCenter2' -ClassName AntiVirusProduct | Where-Object { $_.displayName -notmatch 'Defender' -and (($_.productState -shr 12) -band 0xF) -eq 1 } | ForEach-Object { $_.displayName } | Sort-Object -Unique) $mp = Get-MpComputerStatus $funde = @(Get-MpThreatDetection | Where-Object { $_.InitialDetectionTime -gt $Start.AddDays(-31) }) $pua = (Get-MpPreference).PUAProtection $fwAus = @(Get-NetFirewallProfile | Where-Object { -not $_.Enabled } | ForEach-Object { $_.Name }) $secureBoot = $null; try { $secureBoot = Confirm-SecureBootUEFI -ErrorAction Stop } catch {} $tpm = $null; try { $tpm = Get-Tpm -ErrorAction Stop } catch {} $verschluesselt = $null; try { $verschluesselt = ((Get-BitLockerVolume -MountPoint "$($LW):" -ErrorAction Stop).ProtectionStatus -eq 'On') } catch {} $punkte = @(Get-ComputerRestorePoint | ForEach-Object { [Management.ManagementDateTimeConverter]::ToDateTime($_.CreationTime) } | Sort-Object) # --- Datensicherung: nur Windows-eigene Wege lassen sich erkennen (andere Sicherungsprogramme nicht) --- Fortschritt 'Datensicherung' $sicherungen = New-Object System.Collections.Generic.List[string] $inOneDrive = @(foreach ($f in @(@('Desktop', 'Desktop'), @('MyDocuments', 'Dokumente'), @('MyPictures', 'Bilder'))) { $pf = [Environment]::GetFolderPath($f[0]) if ($pf -and ($oneDrive | Where-Object { $pf -like "$_\*" })) { $f[1] } }) if ($inOneDrive.Count) { $sicherungen.Add("OneDrive sichert: $($inOneDrive -join ', ')") } if (Test-Path (Join-Path $env:LOCALAPPDATA 'Microsoft\Windows\FileHistory\Configuration\Config1.xml')) { $sicherungen.Add('Dateiversionsverlauf ist eingerichtet') } $wb = @(& wbadmin.exe get versions 2>$null | Where-Object { $_ -match '^(Sicherungszeit|Backup time):\s*(.+)$' } | ForEach-Object { $matches[2].Trim() }) if ($wb.Count) { $sicherungen.Add("Systemabbild-Sicherung, zuletzt $($wb[-1])") } # --- Laufwerke (Messwerte brauchen Adminrechte; jeder Lauf schreibt sie in laufwerke-verlauf.csv) --- Fortschritt 'Laufwerke' $verlauf = @(); if (Test-Path $MessDatei) { $verlauf = @(Import-Csv $MessDatei -Delimiter ';') } $laufwerke = @(Get-PhysicalDisk | Where-Object { $_.BusType -ne 'USB' } | ForEach-Object { $rc = $_ | Get-StorageReliabilityCounter [pscustomobject]@{ Datum = $Start.ToString('yyyy-MM-dd'); Laufwerk = $_.FriendlyName; Art = "$($_.MediaType)"; Zustand = "$($_.HealthStatus)" Verschleiss_Prozent = [int]$rc.Wear; Temperatur_C = [int]$rc.Temperature; Temperatur_Max_C = [int]$rc.TemperatureMax Betriebsstunden = [int]$rc.PowerOnHours; Lesefehler = [int]$rc.ReadErrorsTotal; Schreibfehler = [int]$rc.WriteErrorsTotal } }) $laufwerke | Export-Csv $MessDatei -Delimiter ';' -Append -NoTypeInformation -Encoding UTF8 # --- Geräte mit Treiberproblemen (wie das gelbe Warnzeichen im Geräte-Manager; 22 = absichtlich deaktiviert) --- Fortschritt 'Geräte und Treiber' $geraete = @(Get-CimInstance Win32_PnPEntity | Where-Object { $_.ConfigManagerErrorCode -and $_.ConfigManagerErrorCode -notin 22, 45 } | ForEach-Object { $c = [int]$_.ConfigManagerErrorCode [pscustomobject]@{ Name = $(if ($_.Name) { "$($_.Name)" } else { "$($_.DeviceID)" }); Code = $c; Text = $(if ($GeraeteCodes.ContainsKey($c)) { $GeraeteCodes[$c] } else { "Fehlercode $c" }) } }) # --- Stabilität: Ereignisanzeige der letzten 31 Tage. Immer nach Quelle UND Nummer filtern, # dieselben Nummern kommen auch bei harmlosen Quellen vor (z. B. Kernel-Processor-Power 55). --- Fortschritt 'Stabilität (Ereignisanzeige)' $ab = $Start.AddDays(-31) function Ev($log, $quellen, $ids) { @(foreach ($q in $quellen) { Get-WinEvent -FilterHashtable @{ LogName = $log; ProviderName = $q; Id = $ids; StartTime = $ab } -ErrorAction SilentlyContinue }) } $evBlue = Ev 'System' 'Microsoft-Windows-WER-SystemErrorReporting' 1001 $evAus = Ev 'System' 'Microsoft-Windows-Kernel-Power' 41 if (-not $evAus.Count) { $evAus = Ev 'System' 'EventLog' 6008 } $evDisk = @(Ev 'System' 'disk' (7, 11, 51, 153)) + @(Ev 'System' ('Ntfs', 'Microsoft-Windows-Ntfs') 55) + @(Ev 'System' ('storahci', 'stornvme') 129) $evDiskDetails = @($evDisk | Group-Object ProviderName, Id | ForEach-Object { "$($_.Name): $($_.Count)x" }) $evGrafik = Ev 'System' 'Display' 4101 $evApp = Ev 'Application' 'Application Error' 1000 $abstuerze = @($evApp | ForEach-Object { $_.Properties[0].Value } | Group-Object | Sort-Object Count -Descending | Select-Object -First 5 | ForEach-Object { "$($_.Name) ($($_.Count)x)" }) # --- Autostart und Startdauer --- Fortschritt 'Autostart' function AutostartAn($name, $befehl) { # Task-Manager merkt sich »deaktiviert« unter StartupApproved: erstes Byte gerade = an, ungerade = aus foreach ($w in 'HKCU:', 'HKLM:') { foreach ($u in 'Run', 'Run32', 'StartupFolder') { $werte = Get-ItemProperty "$w\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\$u" -ErrorAction SilentlyContinue foreach ($kandidat in @($name, (Split-Path "$befehl" -Leaf))) { $v = $werte.$kandidat if ($v -is [byte[]] -and $v.Count) { return (($v[0] -band 1) -eq 0) } } } } return $true } $autostartListe = @(Get-CimInstance Win32_StartupCommand | Where-Object { $_.Name } | ForEach-Object { [pscustomobject]@{ Name = "$($_.Name)"; Befehl = "$($_.Command)"; Ort = $(if ($_.Location -match 'Startup') { 'Autostart-Ordner' } elseif ($_.Location -match '^HKLM|HKEY_LOCAL') { 'alle Benutzer' } else { 'dieser Benutzer' }); An = (AutostartAn $_.Name $_.Command) } } | Sort-Object Name -Unique) $autostartAn = @($autostartListe | Where-Object An) $startdauer = $null $evStart = Get-WinEvent -FilterHashtable @{ LogName = 'Microsoft-Windows-Diagnostics-Performance/Operational'; Id = 100 } -MaxEvents 1 -ErrorAction SilentlyContinue if ($evStart) { $bt = ([xml]$evStart.ToXml()).Event.EventData.Data | Where-Object { $_.Name -eq 'BootTime' }; if ($bt) { $startdauer = [math]::Round([int]$bt.'#text' / 1000) } } # --- Akku (nur Laptops) --- $akku = $null $bs = Get-CimInstance -Namespace 'root/wmi' -ClassName BatteryStaticData | Select-Object -First 1 $bf = Get-CimInstance -Namespace 'root/wmi' -ClassName BatteryFullChargedCapacity | Select-Object -First 1 if ($bs.DesignedCapacity -gt 0 -and $bf.FullChargedCapacity -gt 0) { $akku = [math]::Min(100, [math]::Round($bf.FullChargedCapacity / $bs.DesignedCapacity * 100)) } # --- Neu seit dem letzten Lauf: Autostart, geplante Aufgaben, Dienste, Programme --- Fortschritt 'Neues seit dem letzten Lauf und Reste' $autostart = @($autostartListe | ForEach-Object { $_.Name }) $aufgaben = @(Get-ScheduledTask | Where-Object { $_.TaskPath -notlike '\Microsoft\*' -and $_.State -ne 'Disabled' } | ForEach-Object { "$($_.TaskPath)$($_.TaskName)" } | Sort-Object -Unique) $dienste = @(Get-CimInstance Win32_Service | Where-Object { $_.StartMode -eq 'Auto' -and $_.PathName -notmatch '\\Windows\\' } | ForEach-Object { "$($_.DisplayName)" } | Sort-Object -Unique) $k = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' $programme = @(Get-ItemProperty $k | Where-Object { $_.DisplayName -and $_.SystemComponent -ne 1 } | ForEach-Object { "$($_.DisplayName)" } | Sort-Object -Unique) function Neu($alt, $neu) { if ($null -eq $alt) { return @() }; @($neu | Where-Object { $_ -notin @($alt) }) } $neuAutostart = Neu $letzter.Autostart $autostart $neuAufgaben = Neu $letzter.Aufgaben $aufgaben $neuDienste = Neu $letzter.Dienste $dienste $neuProgramme = Neu $letzter.Programme $programme $reste = @(Find-Reste) # ---------------- Bewertung (Ampel): ROT = handeln, GELB = empfohlen ---------------- $befunde = New-Object System.Collections.Generic.List[object] function B($stufe, $bereich, $text, $tun) { $befunde.Add([pscustomobject]@{ Stufe = $stufe; Bereich = $bereich; Text = $text; Tun = $tun }) } # Speicher $freiTxt = '{0}: hat nur noch {1:N0} GB frei ({2:N0} %)' -f $LW, $freiGB, $freiPct if ($freiPct -lt 10) { B 'ROT' 'Speicher' $freiTxt 'Große, nicht mehr gebrauchte Dateien und Programme entfernen – siehe »Speicherfresser«' } elseif ($freiPct -lt 20) { B 'GELB' 'Speicher' $freiTxt 'Große, nicht mehr gebrauchte Dateien und Programme entfernen – siehe »Speicherfresser«' } foreach ($v in $andereLW) { $p = $v.SizeRemaining / $v.Size * 100; if ($p -lt 10) { B 'GELB' 'Speicher' ('{0}: hat nur noch {1:N0} GB frei ({2:N0} %)' -f $v.DriveLetter, ($v.SizeRemaining / 1GB), $p) 'Aufräumen' } } if ($papierkorb -gt 2) { B 'GELB' 'Speicher' ('Papierkorb belegt {0:N1} GB' -f $papierkorb) 'Die Pflege leert ihn (fragt vorher)' } if ($tempGB -gt 2) { B 'GELB' 'Speicher' ('Temporäre Dateien: {0:N1} GB' -f $tempGB) 'Die Pflege räumt sie auf' } if ($downloadsGB -gt 10) { B 'GELB' 'Speicher' ('Der Ordner Downloads belegt {0:N1} GB' -f $downloadsGB) 'Durchsehen: Installationsdateien und Erledigtes löschen' } if (-not $speicheroptimierung) { B 'GELB' 'Speicher' 'Die Speicheroptimierung von Windows ist aus' 'Die Pflege bietet an, sie einzuschalten – dann räumt Windows zwischendurch selbst auf' } if ($altWinOrdner.Count -and $altWinAlter -ge 10) { B 'GELB' 'Speicher' ('Reste einer früheren Windows-Version: {0:N1} GB' -f $altWinGB) 'Die Pflege entfernt sie (fragt vorher)' } # Windows if (-not $letztesUpdate -or $letztesUpdate -lt $Start.AddDays(-45)) { B 'GELB' 'Windows' "Letztes kumulatives Windows-Update: $(D $letztesUpdate)" 'Die Pflege sucht und installiert Updates (fragt vorher)' } if ($neustartAus) { B 'GELB' 'Windows' 'Windows wartet auf einen Neustart, um Updates abzuschließen' 'PC neu starten' } if ($os.LastBootUpTime -lt $Start.AddDays(-14)) { B 'GELB' 'Windows' "Seit $(D $os.LastBootUpTime) nicht neu gestartet" 'PC einmal neu starten (Herunterfahren reicht nicht, wenn der Schnellstart an ist)' } if (-not $punkte.Count) { B 'GELB' 'Windows' 'Es gibt keinen Wiederherstellungspunkt' 'Die Pflege legt einen an und bietet an, den Computerschutz einzuschalten' } if ($autostartAn.Count -gt 15) { B 'GELB' 'Windows' "$($autostartAn.Count) Programme starten mit Windows" 'Liste »Autostart« durchsehen; Unnötiges im Task-Manager > Autostart-Apps deaktivieren' } if ($startdauer -gt 120) { B 'GELB' 'Windows' "Der letzte Start dauerte $startdauer Sekunden" 'Autostart-Programme verringern; bei einer Festplatte (HDD) hilft eine SSD am meisten' } # Datensicherung if (-not $sicherungen.Count) { B 'GELB' 'Datensicherung' 'Keine Datensicherung mit Windows-Bordmitteln gefunden' 'Wichtige Dateien sichern: Startmenü > »Windows-Sicherung« (OneDrive) oder Dateiversionsverlauf auf eine externe Festplatte. Sichert schon ein anderes Programm, kann dieser Punkt ignoriert werden' } # Sicherheit if (-not $fremdAV.Count) { # Ist ein anderer Virenschutz aktiv, ruht Defender absichtlich und wird nicht bemängelt if (-not $mp.RealTimeProtectionEnabled) { B 'ROT' 'Sicherheit' 'Der Echtzeitschutz von Microsoft Defender ist aus' 'Windows-Sicherheit > Viren- & Bedrohungsschutz > Einstellungen verwalten' } if ($mp -and -not $mp.IsTamperProtected) { B 'GELB' 'Sicherheit' 'Der Manipulationsschutz ist aus' 'Windows-Sicherheit > Viren- & Bedrohungsschutz > Einstellungen verwalten > Manipulationsschutz' } if ($funde.Count) { B 'ROT' 'Sicherheit' ('Defender hat im letzten Monat {0} Bedrohung(en) gefunden' -f $funde.Count) 'Windows-Sicherheit > Schutzverlauf ansehen' } if (-not $mp.AntivirusSignatureLastUpdated -or $mp.AntivirusSignatureLastUpdated -lt $Start.AddDays(-3)) { B 'GELB' 'Sicherheit' "Virensignaturen vom $(D $mp.AntivirusSignatureLastUpdated)" 'Die Pflege aktualisiert sie' } if (-not $mp.QuickScanEndTime -or $mp.QuickScanEndTime -lt $Start.AddDays(-35)) { B 'GELB' 'Sicherheit' 'Der letzte Schnellscan ist über 35 Tage her' 'Die Pflege macht einen' } if (-not $mp.FullScanEndTime -or $mp.FullScanEndTime -lt $Start.AddDays(-180)) { B 'GELB' 'Sicherheit' "Letzter vollständiger Virenscan: $(D $mp.FullScanEndTime)" 'Windows-Sicherheit > Viren- & Bedrohungsschutz > Scanoptionen > Vollständige Überprüfung (dauert lange, PC bleibt nutzbar)' } if ($pua -eq 0) { B 'GELB' 'Sicherheit' 'Der Schutz vor potenziell unerwünschten Apps ist aus' 'Windows-Sicherheit > App- & Browsersteuerung > Zuverlässigkeitsbasierter Schutz' } } if ($fwAus.Count) { B 'ROT' 'Sicherheit' "Firewall ist aus für: $($fwAus -join ', ')" 'Windows-Sicherheit > Firewall- & Netzwerkschutz' } if ($secureBoot -eq $false) { B 'GELB' 'Sicherheit' 'Secure Boot ist aus' 'Im UEFI-Setup des PCs einschalten. Vorher die Anleitung des Herstellers lesen' } # Laufwerke foreach ($l in $laufwerke) { if ($l.Zustand -ne 'Healthy') { B 'ROT' 'Laufwerke' "$($l.Laufwerk): Zustand $($l.Zustand)" 'SOFORT wichtige Daten sichern, Laufwerk tauschen lassen' } if ($l.Lesefehler -gt 0 -or $l.Schreibfehler -gt 0) { B 'GELB' 'Laufwerke' "$($l.Laufwerk): $($l.Lesefehler) Lese- und $($l.Schreibfehler) Schreibfehler gezählt" 'Datensicherung prüfen, Laufwerk beobachten' } if ($l.Verschleiss_Prozent -ge 80) { B 'ROT' 'Laufwerke' "$($l.Laufwerk): Verschleiß $($l.Verschleiss_Prozent) %" 'Daten sichern, SSD bald ersetzen' } elseif ($l.Verschleiss_Prozent -ge 50) { B 'GELB' 'Laufwerke' "$($l.Laufwerk): Verschleiß $($l.Verschleiss_Prozent) %" 'Datensicherung sicherstellen' } if ($l.Temperatur_C -gt 70) { B 'ROT' 'Laufwerke' "$($l.Laufwerk): $($l.Temperatur_C) °C" 'Lüftung und Staub prüfen' } elseif ($l.Temperatur_C -gt 60) { B 'GELB' 'Laufwerke' "$($l.Laufwerk): $($l.Temperatur_C) °C" 'Lüftung und Staub prüfen' } } $diskHart = @($evDisk | Where-Object { ($_.ProviderName -eq 'disk' -and $_.Id -in 7, 11) -or $_.Id -eq 55 }) if ($diskHart.Count) { B 'ROT' 'Laufwerke' "Laufwerks- oder Dateisystemfehler: $($evDiskDetails -join ', ')" 'SOFORT wichtige Daten sichern; die Pflege prüft das Dateisystem' } elseif ($evDisk.Count) { B 'GELB' 'Laufwerke' "Ein Laufwerk musste Zugriffe wiederholen: $($evDiskDetails -join ', ')" 'Datensicherung prüfen, Kabel prüfen, beobachten' } # Geräte foreach ($g in $geraete) { B 'GELB' 'Geräte' "$($g.Name): $($g.Text)" 'Windows Update > Erweiterte Optionen > Optionale Updates (Treiber); sonst Geräte-Manager > Gerät > Treiber aktualisieren' } # Stabilität if ($evBlue.Count -ge 3) { B 'ROT' 'Stabilität' "$($evBlue.Count) Bluescreens im letzten Monat" 'Treiber aktualisieren, Arbeitsspeicher testen (Startmenü > Windows-Speicherdiagnose)' } elseif ($evBlue.Count) { B 'GELB' 'Stabilität' "$($evBlue.Count) Bluescreen(s) im letzten Monat" 'Beobachten; bei Wiederholung Treiber aktualisieren' } if ($evAus.Count) { B 'GELB' 'Stabilität' "$($evAus.Count) unerwartete Abschaltung(en) im letzten Monat" 'Falls der PC nicht absichtlich hart ausgeschaltet wurde: Stromversorgung und Temperatur prüfen' } if ($evGrafik.Count -ge 3) { B 'GELB' 'Stabilität' "Der Grafiktreiber hat $($evGrafik.Count)-mal nicht reagiert" 'Grafiktreiber über Windows Update oder beim Hersteller aktualisieren' } # Akku if ($akku -and $akku -lt 60) { B 'GELB' 'Akku' "Der Akku hat noch $akku % seiner ursprünglichen Kapazität" 'Normaler Verschleiß. Reicht die Laufzeit nicht mehr, Akku tauschen lassen' } # Neues und Reste foreach ($a in $neuAutostart) { B 'GELB' 'Neu' "Neuer Autostart-Eintrag: $a" 'Falls unbekannt: Task-Manager > Autostart-Apps > deaktivieren' } if ($reste.Count) { B 'GELB' 'Reste' ('{0} Rest(e) deinstallierter Programme' -f $reste.Count) 'Die Pflege entfernt sie (fragt vorher und sichert sie)' } if (-not $letzter.LetztePflege -or [datetime]$letzter.LetztePflege -lt $Start.AddDays(-35)) { B 'GELB' 'Pflege' $(if ($letzter.LetztePflege) { "Letzte Pflege: $(D $letzter.LetztePflege)" } else { 'Die Pflege lief auf diesem PC noch nie' }) 'Jetzt die Pflege starten' } $rot = @($befunde | Where-Object Stufe -eq 'ROT'); $gelb = @($befunde | Where-Object Stufe -eq 'GELB') function StandSpeichern($pflege) { [ordered]@{ Version = $Version; Stand = $Start.ToString('dd.MM.yyyy HH:mm'); LetztePflege = $pflege Autostart = $autostart; Aufgaben = $aufgaben; Dienste = $dienste; Programme = $programme } | ConvertTo-Json -Depth 3 | Set-Content $StandDatei -Encoding UTF8 } StandSpeichern $letzter.LetztePflege # ==================================================================================================== # BERICHT: HTML-Seite, die Microsoft Edge (gehört zu Windows 11) ohne Fenster als PDF druckt. # Klappt das nicht, öffnet sich die HTML-Seite selbst. Beides liegt in %ProgramData%\PC-Pflege\Berichte. # ==================================================================================================== function Html($s) { [Net.WebUtility]::HtmlEncode("$s") } # nicht »H« nennen: h ist in PowerShell die Abkürzung für Get-History # Tabellenzeilen sammeln: Zeilen $liste { @(Spalte1, Spalte2) } - ergibt immer eine Liste von Zeilen, # auch bei nur einer Zeile (PowerShell würde ein einzelnes Array sonst in seine Zellen auflösen) function Zeilen($objekte, [scriptblock]$spalten) { $z = New-Object System.Collections.Generic.List[object] @($objekte) | Where-Object { $null -ne $_ } | ForEach-Object { $z.Add(@(& $spalten)) } return , $z } function Tabelle($kopf, $zeilen) { if ($zeilen.Count -and $zeilen[0] -isnot [array]) { $zeilen = , $zeilen } $t = '' + (($kopf | ForEach-Object { "" }) -join '') + '' foreach ($z in $zeilen) { $t += '' + (($z | ForEach-Object { "" }) -join '') + '' } return $t + '
$(Html $_)
$(Html $_)
' } function Bericht-Html($mitPflege) { $css = @' @page { size: A4; margin: 14mm 13mm; } * { box-sizing: border-box; -webkit-print-color-adjust: exact; print-color-adjust: exact; } body { margin: 0; font: 10pt/1.45 "Segoe UI", system-ui, sans-serif; color: #15191a; } h1, h2, h3, .mono { font-family: "Cascadia Mono", Consolas, monospace; } .kopf { background: #000; color: #e9efe9; padding: 14px 18px; border-radius: 4px; } .kopf h1 { margin: 0; font-size: 18pt; color: #3ddc84; } .kopf p { margin: 4px 0 0; font-size: 9pt; color: #b5c0b8; } .ampel { display: flex; gap: 10px; margin: 14px 0 4px; } .zahl { flex: 1; border: 1px solid #dfe5e0; border-radius: 4px; padding: 8px 12px; } .zahl b { display: block; font: 700 18pt Consolas, monospace; } .zahl.rot b { color: #d93025; } .zahl.gelb b { color: #b7791f; } .zahl.gruen b { color: #1e9e57; } h2 { font-size: 12pt; margin: 20px 0 6px; padding-bottom: 3px; border-bottom: 2px solid #3ddc84; break-after: avoid; } table { width: 100%; border-collapse: collapse; margin: 4px 0 8px; font-size: 9pt; } th { text-align: left; background: #f1f5f2; font-weight: 600; } th, td { padding: 4px 7px; border-bottom: 1px solid #e3e8e4; vertical-align: top; } tr { break-inside: avoid; } .befund td:first-child { width: 14px; } .punkt { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-top: 3px; } .ROT .punkt { background: #d93025; } .GELB .punkt { background: #e5a50a; } .tun { color: #5a6460; font-size: 8.5pt; } .leer { color: #5a6460; font-style: italic; } .protokoll p { margin: 1px 0; font-size: 9pt; } .protokoll .schritt { margin-top: 8px; font: 700 9.5pt Consolas, monospace; } .protokoll .ok::before { content: "✓ "; color: #1e9e57; } .protokoll .warn { color: #a15c00; } .protokoll .frage { color: #5a6460; } .fuss { margin-top: 18px; padding-top: 6px; border-top: 1px solid #dfe5e0; font-size: 8pt; color: #5a6460; } '@ $h = New-Object Text.StringBuilder [void]$h.Append("PC-Pflege-Bericht $(Html $pcName) $($Start.ToString('dd.MM.yyyy'))") [void]$h.Append("

PC-Pflege · Bericht

$(Html $pcName) · $(Html $windows) · $($Start.ToString('dd.MM.yyyy HH:mm')) · Version $Version

") if ($befunde.Count) { [void]$h.Append("
$($rot.Count)rot – bitte handeln
$($gelb.Count)gelb – empfohlen
") } else { [void]$h.Append('
OKalles im grünen Bereich
') } [void]$h.Append('

Was ansteht

') if ($befunde.Count) { [void]$h.Append('') foreach ($b in @($rot) + @($gelb)) { [void]$h.Append("") } [void]$h.Append('
$(Html $b.Bereich)$(Html $b.Text)
→ $(Html $b.Tun)
') } else { [void]$h.Append('

Nichts – alles im grünen Bereich.

') } if ($mitPflege) { [void]$h.Append('

Was die Pflege gemacht hat

') foreach ($e in $Protokoll) { [void]$h.Append("

$(Html $e.Text)

") } [void]$h.Append('
') } $sbTxt = switch ($secureBoot) { $true { 'an' } $false { 'aus' } default { 'k. A.' } } $tpmTxt = if ($tpm) { if ($tpm.TpmReady) { 'bereit' } elseif ($tpm.TpmPresent) { 'vorhanden' } else { 'nicht gefunden' } } else { 'k. A.' } $vsTxt = switch ($verschluesselt) { $true { 'an' } $false { 'aus' } default { 'k. A.' } } $avTxt = if ($fremdAV.Count) { "$($fremdAV -join ', ') (Microsoft Defender ruht)" } else { "Microsoft Defender · Signaturen $(D $mp.AntivirusSignatureLastUpdated) · Schnellscan $(D $mp.QuickScanEndTime) · vollständiger Scan $(D $mp.FullScanEndTime)" } $ueberblick = @( @('Windows', "$windows · letztes Update $(D $letztesUpdate)"), @('Gestartet', "$(D $os.LastBootUpTime)$(if ($startdauer) { " · der Start dauerte $startdauer Sekunden" })"), @('Speicher', ('{0}: {1:N0} von {2:N0} GB frei · Papierkorb {3:N1} GB · temporäre Dateien {4:N1} GB · Speicheroptimierung {5}' -f $LW, $freiGB, $gesamtGB, $papierkorb, $tempGB, $(if ($speicheroptimierung) { 'an' } else { 'aus' }))), @('Virenschutz', $avTxt), @('Schutz', "Firewall $(if ($fwAus.Count) { 'teilweise aus' } else { 'an' }) · Secure Boot $sbTxt · TPM $tpmTxt · Verschlüsselung $($LW): $vsTxt · Wiederherstellungspunkte $($punkte.Count)"), @('Datensicherung', $(if ($sicherungen.Count) { $sicherungen -join ' · ' } else { 'keine Windows-eigene Sicherung gefunden' })), @('Stabilität (31 Tage)', "$($evBlue.Count) Bluescreens · $($evAus.Count) unerwartete Abschaltungen · $($evDisk.Count) Laufwerksfehler · $($evGrafik.Count) Grafiktreiber-Aussetzer · $($evApp.Count) Programmabstürze$(if ($abstuerze.Count) { ': ' + ($abstuerze -join ', ') })") ) if ($akku) { $ueberblick += , @('Akku', "$akku % der ursprünglichen Kapazität") } if ($altWinOrdner.Count) { $ueberblick += , @('Frühere Windows-Version', ('{0:N1} GB, seit {1} Tagen' -f $altWinGB, $altWinAlter)) } $ueberblick += , @('Letzte Pflege', (D $letzter.LetztePflege)) [void]$h.Append('

Überblick

' + (Tabelle @('Bereich', 'Stand') $ueberblick)) $zeilen = New-Object System.Collections.Generic.List[object] foreach ($l in $laufwerke) { $erste = $verlauf | Where-Object { $_.Laufwerk -eq $l.Laufwerk } | Select-Object -First 1 $trend = if ($erste -and $l.Verschleiss_Prozent -and ([datetime]$l.Datum - [datetime]$erste.Datum).Days -ge 30) { " (seit $($erste.Datum): +$($l.Verschleiss_Prozent - [int]$erste.Verschleiss_Prozent) %)" } else { '' } $zeilen.Add(@($l.Laufwerk, $l.Art, $l.Zustand, $(if ($l.Verschleiss_Prozent -or $l.Temperatur_C) { "$($l.Verschleiss_Prozent) %$trend" } else { 'k. A.' }), $(if ($l.Temperatur_C) { "$($l.Temperatur_C) °C" } else { 'k. A.' }), $l.Betriebsstunden, "$($l.Lesefehler) / $($l.Schreibfehler)")) } [void]$h.Append('

Laufwerke

' + (Tabelle @('Laufwerk', 'Art', 'Zustand', 'Verschleiß', 'Temperatur', 'Betriebsstunden', 'Lese-/Schreibfehler') $zeilen)) [void]$h.Append('

Speicherfresser im Benutzerordner

') [void]$h.Append((Tabelle @('Ordner', 'Größe') (Zeilen $topOrdner { $_.Ordner, ('{0:N1} GB' -f $_.GB) }))) if ($topDateien.Count) { [void]$h.Append((Tabelle @('Datei ab 1 GB', 'Größe', 'Geändert') (Zeilen $topDateien { $_.Pfad, ('{0:N1} GB' -f $_.GB), $_.Datum.ToString('dd.MM.yyyy') }))) } [void]$h.Append("

Autostart ($($autostartAn.Count) aktiv)

") if ($autostartListe.Count) { [void]$h.Append((Tabelle @('Programm', 'Status', 'für', 'Befehl') (Zeilen $autostartListe { $_.Name, $(if ($_.An) { 'an' } else { 'aus' }), $_.Ort, $_.Befehl }))) } else { [void]$h.Append('

Keine Autostart-Einträge.

') } [void]$h.Append('

Geräte mit Treiberproblemen

') if ($geraete.Count) { [void]$h.Append((Tabelle @('Gerät', 'Problem', 'Code') (Zeilen $geraete { $_.Name, $_.Text, $_.Code }))) } else { [void]$h.Append('

Keine – alle Geräte arbeiten ohne Fehlermeldung.

') } [void]$h.Append('

Reste deinstallierter Programme

') if ($reste.Count) { [void]$h.Append((Tabelle @('Art', 'Name', 'Grund') (Zeilen $reste { $_.Art, $_.Name, $_.Grund }))) } else { [void]$h.Append('

Keine gefunden.

') } [void]$h.Append('

Neu seit dem letzten Lauf

') if (-not $letzter) { [void]$h.Append('

Erster Lauf auf diesem PC – ab jetzt wird verglichen.

') } else { $neu = New-Object System.Collections.Generic.List[object] foreach ($x in $neuAutostart) { $neu.Add(@('Autostart', $x)) } foreach ($x in $neuDienste) { $neu.Add(@('Dienst', $x)) } foreach ($x in $neuAufgaben) { $neu.Add(@('Geplante Aufgabe', $x)) } foreach ($x in $neuProgramme) { $neu.Add(@('Programm', $x)) } if ($neu.Count) { [void]$h.Append((Tabelle @('Art', 'Name') $neu)) } else { [void]$h.Append("

Nichts Neues seit $(Html $letzter.Stand).

") } } [void]$h.Append("
Erstellt mit PC-Pflege $Version · pcpflege.geofox-it.de · Protokoll: $(Html $LogDatei)
") return $h.ToString() } function Bericht-Zeigen($mitPflege) { $html = Join-Path $BerichtOrdner "$BerichtName.html" $pdf = Join-Path $BerichtOrdner "$BerichtName.pdf" [IO.File]::WriteAllText($html, (Bericht-Html $mitPflege), (New-Object Text.UTF8Encoding $true)) Remove-Item -LiteralPath $pdf -Force -ErrorAction SilentlyContinue $edge = @("${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe", "$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe") | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 if ($edge) { $profilOrdner = Join-Path $env:TEMP 'PC-Pflege-Edge' $argumente = '--headless', '--disable-gpu', '--no-first-run', '--disable-extensions', '--no-pdf-header-footer', "--user-data-dir=`"$profilOrdner`"", "--print-to-pdf=`"$pdf`"", "`"$(([Uri]$html).AbsoluteUri)`"" $prozess = Start-Process -FilePath $edge -ArgumentList $argumente -WindowStyle Hidden -PassThru if ($prozess -and -not $prozess.WaitForExit(90000)) { $prozess | Stop-Process -Force } if (Test-Path -LiteralPath $profilOrdner) { SicherLoeschen (Get-Item -LiteralPath $profilOrdner -Force) } } $datei = if ((Test-Path -LiteralPath $pdf) -and (Get-Item -LiteralPath $pdf).Length -gt 0) { $pdf } else { $html } # Über den Explorer öffnen: so startet der Betrachter als normaler Benutzer, nicht mit Administratorrechten Start-Process explorer.exe -ArgumentList "`"$datei`"" return $datei } Write-Host '' if (-not $befunde.Count) { Write-Host '● Alles im grünen Bereich.' -ForegroundColor Green } else { Write-Host ('● {0} rot, {1} gelb' -f $rot.Count, $gelb.Count) -ForegroundColor $(if ($rot.Count) { 'Red' } else { 'Yellow' }) } Write-Host 'Der Bericht wird erstellt und gleich geöffnet ...' -ForegroundColor DarkGray $bericht = Bericht-Zeigen $false Write-Host "Bericht: $bericht" -ForegroundColor DarkGray Write-Host '' if (-not (Frage 'Teil 2 starten: den PC jetzt pflegen? (15–45 Minuten, einzelne Schritte fragen nach)')) { Stop-Transcript | Out-Null Read-Host ' Enter drücken zum Schließen' | Out-Null return } # ==================================================================================================== # TEIL 2: PFLEGEN # ==================================================================================================== $Sammeln = $true $freiVorher = (Get-Volume -DriveLetter $LW).SizeRemaining $neustart = $false Write-Host '' Write-Host 'Teil 2: Pflege. Bitte das Fenster offen lassen, sonst bricht sie ab.' -ForegroundColor Cyan # 0) Wiederherstellungspunkt als Sicherheitsnetz vor dem Löschen und vor Updates Schritt '0/12 Wiederherstellungspunkt' function NeuerPunkt { $hinweis = $null Checkpoint-Computer -Description ('PC-Pflege {0:yyyy-MM-dd}' -f (Get-Date)) -RestorePointType MODIFY_SETTINGS -ErrorAction Stop -WarningVariable hinweis -WarningAction SilentlyContinue $zuletzt = Get-ComputerRestorePoint | Select-Object -Last 1 $wann = if ($zuletzt) { D ([Management.ManagementDateTimeConverter]::ToDateTime($zuletzt.CreationTime)) } else { '?' } if ($hinweis) { Info "Kein neuer Punkt: Windows erlaubt nur einen pro 24 Stunden. Letzter Punkt: $wann" } else { Ok "Wiederherstellungspunkt erstellt ($wann)" } } Info 'Wird erstellt (bis zu 2 Minuten) ...' try { NeuerPunkt } catch { Warn "Kein Wiederherstellungspunkt möglich: $($_.Exception.Message)" if (Frage "Computerschutz für $($LW): einschalten? (nötig für Wiederherstellungspunkte, belegt einige GB)") { try { Enable-ComputerRestore -Drive "$($LW):\" -ErrorAction Stop; NeuerPunkt } catch { Warn "Fehlgeschlagen: $($_.Exception.Message)" } } else { Info 'übersprungen' } } # 1) Temporäre Dateien, Absturzberichte, Zwischenspeicher der Übermittlungsoptimierung Schritt '1/12 Temporäre Dateien' $vorher = (Get-Volume -DriveLetter $LW).SizeRemaining AlteLoeschen $env:TEMP 2 AlteLoeschen "$env:SystemRoot\Temp" 2 AlteLoeschen (Join-Path $env:LOCALAPPDATA 'CrashDumps') 30 AlteLoeschen (Join-Path $env:LOCALAPPDATA 'Microsoft\Windows\WER\ReportArchive') 30 AlteLoeschen "$env:ProgramData\Microsoft\Windows\WER\ReportArchive" 30 AlteLoeschen "$env:ProgramData\Microsoft\Windows\WER\ReportQueue" 30 # Übermittlungsoptimierung = Zwischenspeicher für Windows-Updates, wird bei Bedarf neu geladen try { Delete-DeliveryOptimizationCache -Force -ErrorAction Stop; Info 'Zwischenspeicher der Übermittlungsoptimierung geleert' } catch { Info "Übermittlungsoptimierung: $($_.Exception.Message)" } Ok ('{0:N2} GB freigegeben' -f (((Get-Volume -DriveLetter $LW).SizeRemaining - $vorher) / 1GB)) # 2) Papierkorb Schritt '2/12 Papierkorb' $pk = PapierkorbGB Info ('Größe: {0:N2} GB' -f $pk) if ($pk -ge 0.05) { if (Frage 'Papierkorb endgültig leeren?') { Clear-RecycleBin -Force -ErrorAction SilentlyContinue; Ok 'geleert' } else { Info 'übersprungen' } } else { Info 'leer' } # 3) Reste früherer Windows-Versionen - über die Datenträgerbereinigung von Windows (cleanmgr), die auch # die geschützten Dateien in Windows.old entfernen kann. Danach ist kein Zurück zur vorherigen Version mehr möglich. Schritt '3/12 Reste früherer Windows-Versionen' if (-not $altWinOrdner.Count) { Ok 'keine vorhanden' } elseif ($altWinAlter -lt 10) { Info ('{0:N1} GB, erst {1} Tage alt - bleiben vorerst, damit Windows noch zur vorherigen Version zurückkehren kann' -f $altWinGB, $altWinAlter) } else { Info ('{0:N1} GB in {1}' -f $altWinGB, ($altWinOrdner -join ', ')) if (Frage 'Entfernen? (danach ist kein Zurück zur vorherigen Windows-Version möglich)') { $vc = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches' $kategorien = @('Previous Installations', 'Temporary Setup Files', 'Windows Upgrade Log Files' | Where-Object { Test-Path "$vc\$_" }) foreach ($kat in $kategorien) { Set-ItemProperty "$vc\$kat" -Name StateFlags0777 -Value 2 -Type DWord } Info 'Datenträgerbereinigung läuft (einige Minuten, es öffnet sich ein kleines Fenster) ...' Start-Process cleanmgr.exe -ArgumentList '/sagerun:777' -Wait foreach ($kat in $kategorien) { Remove-ItemProperty "$vc\$kat" -Name StateFlags0777 -ErrorAction SilentlyContinue } $rest = @($altWinOrdner | Where-Object { Test-Path -LiteralPath $_ }) if ($rest.Count) { Warn "Nicht vollständig entfernt: $($rest -join ', ')" } else { Ok ('{0:N1} GB entfernt' -f $altWinGB) } } else { Info 'übersprungen' } } # 4) Speicheroptimierung: Windows räumt dann zwischen den Läufen selbst auf (Temp-Dateien, Papierkorb nach 30 Tagen) Schritt '4/12 Speicheroptimierung' if ($speicheroptimierung) { Ok 'ist bereits eingeschaltet' } elseif (Frage 'Speicheroptimierung einschalten? (Windows räumt dann regelmäßig selbst temporäre Dateien auf)') { if (-not (Test-Path $SpPfad)) { New-Item $SpPfad -Force | Out-Null } Set-ItemProperty $SpPfad -Name '01' -Value 1 -Type DWord Ok 'eingeschaltet (ändern: Einstellungen > System > Speicher)' } else { Info 'übersprungen' } # 5) Windows-Updates (über die Windows-Update-Schnittstelle, wie unter Einstellungen > Windows Update) Schritt '5/12 Windows-Updates' Info 'Suche nach Updates (kann einige Minuten dauern) ...' try { $session = New-Object -ComObject Microsoft.Update.Session $session.ClientApplicationID = 'PC-Pflege' $ergebnis = $session.CreateUpdateSearcher().Search('IsInstalled=0 and IsHidden=0') $liste = New-Object -ComObject Microsoft.Update.UpdateColl foreach ($u in $ergebnis.Updates) { if (-not $u.BrowseOnly) { [void]$liste.Add($u); Info "- $($u.Title)" } } # optionale Updates bleiben außen vor if ($liste.Count -eq 0) { Ok 'Windows ist auf dem neuesten Stand' } elseif (Frage "Diese $($liste.Count) Update(s) jetzt herunterladen und installieren?") { foreach ($u in $liste) { if (-not $u.EulaAccepted) { $u.AcceptEula() } } Info 'Herunterladen ...' $lader = $session.CreateUpdateDownloader(); $lader.Updates = $liste; [void]$lader.Download() Info 'Installieren ...' $inst = $session.CreateUpdateInstaller(); $inst.Updates = $liste; $res = $inst.Install() for ($i = 0; $i -lt $liste.Count; $i++) { $code = $res.GetUpdateResult($i).ResultCode # 2 = erfolgreich, 3 = mit Fehlern, 4 = fehlgeschlagen if ($code -eq 2) { Ok $liste.Item($i).Title } else { Warn "Nicht installiert (Code $code): $($liste.Item($i).Title)" } } if ($res.RebootRequired) { $neustart = $true; Warn 'Für diese Updates ist ein Neustart nötig.' } } else { Info 'übersprungen' } } catch { Warn "Windows Update: $($_.Exception.Message)" } # 6) Virenschutz Schritt '6/12 Microsoft Defender' if ($fremdAV.Count) { Info "Übersprungen: Es ist ein anderer Virenschutz aktiv ($($fremdAV -join ', '))." } else { try { Update-MpSignature -ErrorAction Stop; Ok 'Virensignaturen aktualisiert' } catch { Warn "Signaturen: $($_.Exception.Message)" } Info 'Schnellscan läuft (einige Minuten) ...' try { Start-MpScan -ScanType QuickScan -ErrorAction Stop; Ok 'Schnellscan fertig' } catch { Warn "Scan: $($_.Exception.Message)" } $funde = @(Get-MpThreatDetection | Where-Object { $_.InitialDetectionTime -gt (Get-Date).AddDays(-31) }) if ($funde.Count) { Warn "ACHTUNG: $($funde.Count) Fund(e) im letzten Monat - Windows-Sicherheit > Schutzverlauf öffnen!" } else { Ok 'keine Funde im letzten Monat' } } # 7) Reste deinstallierter Programme Schritt '7/12 Reste deinstallierter Programme' if ($reste.Count) { for ($i = 0; $i -lt $reste.Count; $i++) { Write-Host (' {0,2}) [{1}] {2}' -f ($i + 1), $reste[$i].Art, $reste[$i].Name) Write-Host (' {0}' -f $reste[$i].Grund) -ForegroundColor DarkGray } $a = Read-Host ' Entfernen? j = alle, n = keine, oder Nummern (z. B. 1,3)' $wahl = if ($a -match '^\s*(j|ja|y|yes)\s*$') { 0..($reste.Count - 1) } elseif ($a -match '^[\d,\s]+$') { @($a -split '[,\s]+' | Where-Object { $_ } | ForEach-Object { [int]$_ - 1 } | Where-Object { $_ -ge 0 -and $_ -lt $reste.Count }) } else { @() } if ($wahl.Count) { $sicherung = Join-Path $SicherOrdner ('Reste-{0:yyyy-MM-dd_HHmm}' -f (Get-Date)) foreach ($i in $wahl) { try { Remove-Rest $reste[$i] $sicherung; Ok "entfernt: $($reste[$i].Name)" } catch { Warn "$($reste[$i].Name): $($_.Exception.Message)" } } Info "Sicherung: $sicherung" } else { Info "$($reste.Count) Rest(e) gefunden, übersprungen" } } else { Ok 'keine Reste gefunden' } # 8) Datenschutz-Spuren von Windows: zuletzt verwendete Dateien, Sprunglisten, Ausführen- und Explorer-Verlauf. # Danach sind auch die praktischen Listen »Zuletzt verwendet« leer - deshalb nur auf Nachfrage. Schritt '8/12 Datenschutz-Spuren von Windows' $zuletzt = Join-Path $env:APPDATA 'Microsoft\Windows\Recent' $spurDateien = @(Get-ChildItem -LiteralPath $zuletzt -File -Force -ErrorAction SilentlyContinue) + @(Get-ChildItem -LiteralPath (Join-Path $zuletzt 'AutomaticDestinations'), (Join-Path $zuletzt 'CustomDestinations') -File -Force -ErrorAction SilentlyContinue) Info "$($spurDateien.Count) Einträge in »Zuletzt verwendet« und den Sprunglisten der Taskleiste, dazu Ausführen-, Adress- und Suchverlauf des Explorers" if (Frage 'Löschen? (danach sind auch die Listen »Zuletzt verwendet« in Explorer und Taskleiste leer)') { $spurDateien | Remove-Item -Force -ErrorAction SilentlyContinue $ex = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer' foreach ($s in 'RunMRU', 'TypedPaths', 'WordWheelQuery', 'RecentDocs') { Remove-Item "$ex\$s" -Recurse -Force -ErrorAction SilentlyContinue } Ok 'gelöscht (vollständig wirksam nach der nächsten Anmeldung)' } else { Info 'übersprungen' } # 9) Laufwerke optimieren: SSDs bekommen ein Trim. Festplatten defragmentiert Windows selbst jede Woche. Schritt '9/12 Laufwerke optimieren' foreach ($v in (Get-Volume | Where-Object { $_.DriveLetter -and $_.DriveType -eq 'Fixed' -and $_.FileSystem -eq 'NTFS' })) { try { Optimize-Volume -DriveLetter $v.DriveLetter -ReTrim -ErrorAction Stop; Ok "$($v.DriveLetter): Trim fertig" } catch { Info "$($v.DriveLetter): kein Trim nötig (keine SSD) oder nicht möglich" } } # 10) Systemdateien prüfen und reparieren (Windows-Bordmittel chkdsk, DISM, SFC) Schritt '10/12 Systemdateien prüfen' if (Frage 'Dateisystem und Windows-Dateien jetzt prüfen und reparieren? (10–30 Minuten, der PC bleibt nutzbar)') { Info "Dateisystem $($LW): (chkdsk /scan) ..." & chkdsk.exe "$($LW):" /scan | Select-Object -Last 4 | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray } if ($LASTEXITCODE -eq 0) { Ok 'Dateisystem ohne Fehler' } else { Warn "chkdsk meldet $LASTEXITCODE - Einzelheiten im Protokoll" } Info 'Windows-Abbild (DISM /RestoreHealth) ...' $p = Start-Process dism.exe -ArgumentList '/Online', '/Cleanup-Image', '/RestoreHealth' -Wait -PassThru -NoNewWindow if ($p.ExitCode -eq 0) { Ok 'Windows-Abbild in Ordnung' } else { Warn "DISM meldet $($p.ExitCode)" } Info 'Systemdateien (sfc /scannow) ...' $p = Start-Process sfc.exe -ArgumentList '/scannow' -Wait -PassThru -NoNewWindow if ($p.ExitCode -eq 0) { Ok 'Systemdateien geprüft' } else { Warn "SFC meldet $($p.ExitCode) - Einzelheiten im Protokoll" } $neustart = $true } else { Info 'übersprungen' } # 11) Alte Update-Dateien im Komponentenspeicher (WinSxS) entfernen Schritt '11/12 Alte Windows-Update-Dateien (5–15 Minuten)' $p = Start-Process dism.exe -ArgumentList '/Online', '/Cleanup-Image', '/StartComponentCleanup' -Wait -PassThru -NoNewWindow if ($p.ExitCode -eq 0) { Ok 'bereinigt' } else { Warn "DISM meldet $($p.ExitCode)" } # 12) Eigene alte Dateien: Berichte, Protokolle und Sicherungen, die älter als ein Jahr sind Schritt '12/12 Alte Berichte und Protokolle der PC-Pflege' $vorEinemJahr = (Get-Date).AddDays(-365) $alt = @(Get-ChildItem -LiteralPath $LogOrdner, $BerichtOrdner, $SicherOrdner -Force -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt $vorEinemJahr }) foreach ($e in $alt) { SicherLoeschen $e } Ok $(if ($alt.Count) { "$($alt.Count) Einträge älter als ein Jahr entfernt" } else { 'nichts älter als ein Jahr' }) # Zusammenfassung Schritt 'Fertig' $freiNachher = (Get-Volume -DriveLetter $LW).SizeRemaining Ok ('{0}: vorher {1:N1} GB frei, jetzt {2:N1} GB (Updates belegen zum Teil neuen Platz)' -f $LW, ($freiVorher / 1GB), ($freiNachher / 1GB)) if ($neustart -or (NeustartAussteht)) { Warn 'Bitte den PC jetzt neu starten.' } else { Info 'Ein Neustart schadet nicht.' } Info 'Nächste Pflege: in etwa einem Monat. Neue Version und Anleitung: pcpflege.geofox-it.de' $Sammeln = $false StandSpeichern ((Get-Date).ToString('yyyy-MM-dd HH:mm')) Write-Host '' Write-Host 'Der Bericht wird um das Ergebnis der Pflege ergänzt und geöffnet ...' -ForegroundColor DarkGray $bericht = Bericht-Zeigen $true Write-Host "Bericht: $bericht" -ForegroundColor DarkGray Stop-Transcript | Out-Null Read-Host ' Enter drücken zum Schließen' | Out-Null return <# MIT License Copyright (c) 2026 Janek Voß Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #>