# JDS Kit — diagnostic + boîte à outils (app unifiée) # Lancement: irm https://tool.fenrir.pw | iex ou irm https://tool.fenrir.pw/kit | iex # Version: 2.1.0 $script:JdsVersion = '2.1.0' $script:JdsInstallDir = Join-Path $env:LOCALAPPDATA 'JDS\Install' $script:JdsUninstallRegPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\JDSKit' $script:JdsShortcutName = 'JDS Kit.lnk' $script:JdsOptionsPath = Join-Path $env:LOCALAPPDATA 'JDS\options.json' $script:JdsWorkshopApiDefaultUrl = 'https://dash.informatiquejds.com/api' $script:JdsWorkshopApiDefaultKey = 'jds-default-api-key-2026' $script:JdsDashboardWorkshopStatuses = @('nouveau', 'john', 'steve', 'steven') $script:JdsLaunchUrl = 'https://tool.fenrir.pw' $script:JdsKitLaunchUrl = 'https://tool.fenrir.pw/kit' $script:JdsKitCatalogUrl = 'https://tool.fenrir.pw/kit-catalog.zip' $script:JdsKitRoot = Join-Path $env:LOCALAPPDATA 'JDS\Kit' $script:JdsKitCatalog = Join-Path $script:JdsKitRoot 'Catalog' $script:JdsKitApps = Join-Path $script:JdsKitRoot 'Apps' $script:JdsCachePath = Join-Path $env:LOCALAPPDATA 'JDS\jds.ps1' $script:JdsLiveProcess = $null $script:JdsLiveEvents = @() $script:JdsConsoleQueue = $null $ErrorActionPreference = 'Continue' $ProgressPreference = 'SilentlyContinue' # --------------------------------------------------------------------------- # Élévation admin + STA (WPF) # --------------------------------------------------------------------------- function Test-JdsAdmin { $id = [Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object Security.Principal.WindowsPrincipal($id) return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Get-JdsProcessPath { try { return [Diagnostics.Process]::GetCurrentProcess().MainModule.FileName } catch { return $null } } function Save-JdsScriptCache { try { if ($PSCommandPath -and (Test-Path -LiteralPath $PSCommandPath)) { $dir = Split-Path $script:JdsCachePath New-Item -ItemType Directory -Path $dir -Force | Out-Null Copy-Item -LiteralPath $PSCommandPath -Destination $script:JdsCachePath -Force return $true } } catch { } return $false } function Update-JdsScriptCacheFromRemote { try { $dir = Split-Path $script:JdsCachePath New-Item -ItemType Directory -Path $dir -Force | Out-Null $r = Invoke-WebRequest -Uri $script:JdsLaunchUrl -UseBasicParsing -TimeoutSec 20 $body = $r.Content if ($body.Length -gt 80 -and $body[0] -eq [char]0xFEFF) { $body = $body.Substring(1) } if ($body -notmatch 'function Get-JdsOsInfo') { return $false } [IO.File]::WriteAllText($script:JdsCachePath, $body, (New-Object System.Text.UTF8Encoding $true)) return $true } catch { return $false } } function Get-JdsElevatedScriptPath { if ($PSCommandPath -and (Test-Path -LiteralPath $PSCommandPath)) { return $PSCommandPath } # irm | iex : pas de fichier local — toujours rafraîchir le cache avant élévation admin if (-not $PSCommandPath) { if (Update-JdsScriptCacheFromRemote) { return $script:JdsCachePath } } elseif (-not (Test-Path -LiteralPath $script:JdsCachePath)) { Update-JdsScriptCacheFromRemote | Out-Null } if (Test-Path -LiteralPath $script:JdsCachePath) { return $script:JdsCachePath } try { if (Update-JdsScriptCacheFromRemote) { return $script:JdsCachePath } } catch { return $null } return $null } function Start-JdsElevated { $exe = Get-JdsProcessPath $isOwnExe = $exe -and ($exe -match '\.exe$') -and ($exe -notmatch '(?i)\\(powershell|pwsh|powershell_ise)\.exe$') if ($isOwnExe) { Start-Process -FilePath $exe -Verb RunAs | Out-Null return } $boot = Get-JdsElevatedScriptPath if ($boot) { Start-Process -FilePath 'powershell.exe' -Verb RunAs -ArgumentList @( '-STA', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $boot ) | Out-Null return } Start-Process -FilePath 'powershell.exe' -Verb RunAs -ArgumentList @( '-STA', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', "irm $script:JdsLaunchUrl | iex" ) | Out-Null } $sta = [Threading.Thread]::CurrentThread.GetApartmentState() $needRelaunch = -not (Test-JdsAdmin) -or ($sta -ne 'STA') if ($needRelaunch) { if (-not (Test-JdsAdmin)) { Write-Host 'JDS: elevation administrateur requise...' } Start-JdsElevated exit } Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase, System.Windows.Forms, Microsoft.VisualBasic | Out-Null function Hide-JdsConsole { try { $def = @' using System; using System.Runtime.InteropServices; public static class JdsConsole { [DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow(); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); } '@ Add-Type -TypeDefinition $def -ErrorAction SilentlyContinue $hwnd = [JdsConsole]::GetConsoleWindow() if ($hwnd -ne [IntPtr]::Zero) { [JdsConsole]::ShowWindow($hwnd, 0) | Out-Null } } catch { } } # --------------------------------------------------------------------------- # Collecte # --------------------------------------------------------------------------- function Get-JdsSafe([scriptblock]$Block, $Fallback = '') { try { & $Block } catch { $Fallback } } function Get-JdsLicenseStatusText([int]$Code) { switch ($Code) { 0 { 'Non licencié' } 1 { 'Licencié (activé)' } 2 { 'Grace OOB' } 3 { 'Grace OOT' } 4 { 'Grace non authentique' } 5 { 'Notification' } 6 { 'Grace étendue' } default { "Inconnu ($Code)" } } } function Get-JdsOsInfo { $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop $cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop $cv = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction Stop $boot = $os.LastBootUpTime $uptime = (Get-Date) - $boot $arch = if ([Environment]::Is64BitOperatingSystem) { '64 bits' } else { '32 bits' } $display = $cv.DisplayVersion if (-not $display) { $display = $cv.ReleaseId } $ubr = $cv.UBR $buildFull = if ($null -ne $ubr) { "$($cv.CurrentBuild).$ubr" } else { [string]$cv.CurrentBuild } [pscustomobject]@{ ComputerName = $cs.Name User = "$env:USERDOMAIN\$env:USERNAME" Domain = $cs.Domain PartOfDomain = [bool]$cs.PartOfDomain Manufacturer = $cs.Manufacturer Model = $cs.Model Caption = $os.Caption EditionId = $cv.EditionID DisplayVersion = $display Build = $buildFull CurrentBuild = $cv.CurrentBuild InstallDate = $os.InstallDate LastBoot = $boot Uptime = '{0}j {1}h {2}min' -f [int]$uptime.TotalDays, $uptime.Hours, $uptime.Minutes Arch = $arch InstallType = $cv.InstallationType RegisteredOwner = $cv.RegisteredOwner ProductName = $cv.ProductName OsVersion = $os.Version TotalRamGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1) } } function Get-JdsWindowsLicense { $winAppId = '55c92734-d682-4d71-983e-d6ec3f16059f' $products = @(Get-CimInstance -ClassName SoftwareLicensingProduct -Filter "ApplicationID='$winAppId'" -ErrorAction SilentlyContinue | Where-Object { $_.PartialProductKey }) $p = $products | Select-Object -First 1 if (-not $p) { return [pscustomobject]@{ Found = $false Name = 'Introuvable' Description = '' Status = 'Inconnu' StatusCode = -1 Licensed = $false PartialKey = '' Channel = '' RemainingGrace = '' } } $grace = '' if ($p.GracePeriodRemaining -and $p.GracePeriodRemaining -gt 0) { $days = [math]::Round($p.GracePeriodRemaining / 1440, 1) $grace = "$days jours" } [pscustomobject]@{ Found = $true Name = $p.Name Description = $p.Description Status = Get-JdsLicenseStatusText ([int]$p.LicenseStatus) StatusCode = [int]$p.LicenseStatus Licensed = ([int]$p.LicenseStatus -eq 1) PartialKey = $p.PartialProductKey Channel = $p.ProductKeyChannel RemainingGrace = $grace EvaluationEnd = $p.EvaluationEndDate } } function Get-JdsOfficeReleaseIdName([string]$Id) { if (-not $Id) { return '' } if ($script:JdsOfficeReleaseMap -and $script:JdsOfficeReleaseMap.ContainsKey($Id)) { return $script:JdsOfficeReleaseMap[$Id] } $map = @{ 'O365ProPlusRetail' = 'Microsoft 365 Apps' 'O365ProPlusBusinessRetail' = 'Microsoft 365 Apps for business' 'O365BusinessRetail' = 'Microsoft 365 Business' 'O365HomePremRetail' = 'Microsoft 365 Famille' 'ProPlus2021Volume' = 'Microsoft Office Professional Plus 2021' 'ProPlus2021Retail' = 'Microsoft Office Professional Plus 2021' 'ProPlus2019Volume' = 'Microsoft Office Professional Plus 2019' 'ProPlus2019Retail' = 'Microsoft Office Professional Plus 2019' 'ProPlusRetail' = 'Microsoft Office Professional Plus 2016' 'ProPlusVolume' = 'Microsoft Office Professional Plus 2016' 'Standard2021Volume' = 'Microsoft Office Standard 2021' 'Standard2019Volume' = 'Microsoft Office Standard 2019' 'StandardRetail' = 'Microsoft Office Standard 2016' 'HomeBusiness2021Retail' = 'Microsoft Office Home and Business 2021' 'HomeBusiness2019Retail' = 'Microsoft Office Home and Business 2019' 'HomeStudent2021Retail' = 'Microsoft Office Home and Student 2021' 'HomeStudent2019Retail' = 'Microsoft Office Home and Student 2019' 'Access2021Volume' = 'Microsoft Access 2021' 'Excel2021Volume' = 'Microsoft Excel 2021' 'Outlook2021Volume' = 'Microsoft Outlook 2021' 'PowerPoint2021Volume' = 'Microsoft PowerPoint 2021' 'Word2021Volume' = 'Microsoft Word 2021' } $script:JdsOfficeReleaseMap = $map if ($map.ContainsKey($Id)) { return $map[$Id] } if ($Id -match '(?i)(ProPlus|ProfessionalPlus)(?20\d{2})') { return "Microsoft Office Professional Plus $($Matches['y'])" } if ($Id -match '(?i)Standard(?20\d{2})') { return "Microsoft Office Standard $($Matches['y'])" } return $Id } function Get-JdsOfficeYearFromText([string]$Text) { if (-not $Text) { return $null } if ($Text -match '(?i)Microsoft\s+365|Office\s+365|\b365\b') { return '365' } if ($Text -match '(?i)Office\s+XP|\bXP\b') { return 2002 } if ($Text -match '\b(20\d{2}|19\d{2})\b') { return [int]$Matches[1] } if ($Text -match '(?i)\b2003\b') { return 2003 } if ($Text -match '(?i)\b2000\b') { return 2000 } if ($Text -match '(?i)\b97\b') { return 1997 } return $null } function Get-JdsOfficeYearFromMajor([string]$Major, [string]$Version) { $build = 0 if ($Version -match '\.(?\d{4,})') { [void][int]::TryParse($Matches['b'], [ref]$build) } switch ($Major) { '16.0' { if ($build -ge 14000) { return 2021 } if ($build -ge 10000) { return 2019 } return 2016 } '15.0' { return 2013 } '14.0' { return 2010 } '12.0' { return 2007 } '11.0' { return 2003 } '10.0' { return 2002 } '9.0' { return 2000 } '8.0' { return 1997 } } return $null } function Get-JdsOfficeEditionFromName([string]$Name) { if (-not $Name) { return '' } if ($Name -match '(?i)Professional\s+Plus|ProPlus') { return 'Professional Plus' } if ($Name -match '(?i)Home\s+and\s+Business') { return 'Home and Business' } if ($Name -match '(?i)Home\s+and\s+Student|Home\s+&\s+Student') { return 'Home and Student' } if ($Name -match '(?i)\bStandard\b') { return 'Standard' } if ($Name -match '(?i)\bEnterprise\b') { return 'Enterprise' } if ($Name -match '(?i)Microsoft\s+365') { return 'Microsoft 365' } if ($Name -match '(?i)\bWord\b') { return 'Word' } if ($Name -match '(?i)\bExcel\b') { return 'Excel' } if ($Name -match '(?i)\bOutlook\b') { return 'Outlook' } if ($Name -match '(?i)\bAccess\b') { return 'Access' } if ($Name -match '(?i)\bPowerPoint\b') { return 'PowerPoint' } if ($Name -match '(?i)Small\s+Business') { return 'Small Business' } return '' } function Test-JdsOfficeUninstallName([string]$Name) { if (-not $Name) { return $false } if ($Name -notmatch '(?i)^(Microsoft\s+)?(Office|365)') { return $false } if ($Name -match '(?i)Proof|Language\s+Pack|Font|Update|Hotfix|Component|Extensibility|Licensing|ODBC|Help Viewer|File Validation|Groove|InfoPath|Lync|Skype|OneNote') { return $false } return $true } function Get-JdsOfficeScore([string]$Name, [string]$InstallType) { $score = 0 if ($InstallType -eq 'Click-to-Run') { $score += 200 } if ($InstallType -eq 'MSI') { $score += 150 } if ($Name -match '(?i)Professional\s+Plus|ProPlus') { $score += 80 } elseif ($Name -match '(?i)Standard|Home\s+and|Microsoft\s+365|Office\s+365') { $score += 70 } elseif ($Name -match '(?i)Word|Excel|Outlook|PowerPoint|Access') { $score += 30 } $year = Get-JdsOfficeYearFromText $Name if ($year -eq '365') { $score += 90 } elseif ($year) { $score += [int]$year } if ($Name -match '(?i)Volume|Retail') { $score += 5 } return $score } function Add-JdsOfficeCandidate { param( $List, [string]$DisplayName, [string]$Version = '', [string]$InstallType = '', [string]$Source = '', [string]$Channel = '', [string]$ClientFolder = '' ) if (-not $DisplayName) { return } $year = Get-JdsOfficeYearFromText $DisplayName if (-not $year -and $Version) { if ($Version -match '^(?\d+\.\d+)') { $year = Get-JdsOfficeYearFromMajor $Matches['maj'] $Version } } $edition = Get-JdsOfficeEditionFromName $DisplayName $score = Get-JdsOfficeScore $DisplayName $InstallType $List.Add([pscustomobject]@{ DisplayName = $DisplayName.Trim() Year = $year Edition = $edition Version = $Version InstallType = $InstallType Source = $Source Channel = $Channel ClientFolder = $ClientFolder Score = $score }) | Out-Null } function Get-JdsOfficeInfo { $candidates = New-Object System.Collections.Generic.List[object] $c2r = $null $c2rPath = 'HKLM:\SOFTWARE\Microsoft\Office\ClickToRun\Configuration' if (Test-Path $c2rPath) { $c2r = Get-ItemProperty -Path $c2rPath -ErrorAction SilentlyContinue $ver = [string]$c2r.VersionToReport $channel = [string]$c2r.UpdateChannel $folder = [string]$c2r.ClientFolder $ids = @([string]$c2r.ProductReleaseIds -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) if ($ids.Count -eq 0 -and $c2r.ProductReleaseId) { $ids = @([string]$c2r.ProductReleaseId) } foreach ($id in $ids) { $name = Get-JdsOfficeReleaseIdName $id Add-JdsOfficeCandidate -List $candidates -DisplayName $name -Version $ver -InstallType 'Click-to-Run' -Source "C2R:$id" -Channel $channel -ClientFolder $folder } if ($ids.Count -gt 1) { $suiteName = ($ids | ForEach-Object { Get-JdsOfficeReleaseIdName $_ } | Select-Object -Unique) -join ' + ' Add-JdsOfficeCandidate -List $candidates -DisplayName $suiteName -Version $ver -InstallType 'Click-to-Run' -Source 'C2R:bundle' -Channel $channel -ClientFolder $folder } } $uninstallPaths = @( 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' ) foreach ($up in $uninstallPaths) { Get-ItemProperty -Path $up -ErrorAction SilentlyContinue | ForEach-Object { if (-not (Test-JdsOfficeUninstallName $_.DisplayName)) { return } Add-JdsOfficeCandidate -List $candidates -DisplayName $_.DisplayName -Version $_.DisplayVersion -InstallType 'MSI' -Source 'Uninstall' } } foreach ($major in @('16.0','15.0','14.0','12.0','11.0','10.0','9.0','8.0')) { foreach ($root in @("HKLM:\SOFTWARE\Microsoft\Office\$major", "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Office\$major")) { if (-not (Test-Path $root)) { continue } $installRoot = Join-Path $root 'Common\InstallRoot' if (-not (Test-Path $installRoot)) { continue } $regPath = Join-Path $root 'Registration' $regFound = $false if (Test-Path $regPath) { Get-ChildItem -Path $regPath -ErrorAction SilentlyContinue | ForEach-Object { $p = Get-ItemProperty -Path $_.PSPath -ErrorAction SilentlyContinue $prod = [string]$p.ProductName if (-not $prod) { $prod = [string]$p.ProductIcon } if ($prod -and $prod -notmatch '(?i)proof|update') { $regFound = $true $ver = [string]$p.Version if (-not $ver) { $ver = $major } Add-JdsOfficeCandidate -List $candidates -DisplayName $prod -Version $ver -InstallType 'Legacy' -Source "Reg:$major" } } } if (-not $regFound -and $major -in @('11.0','10.0','9.0','8.0')) { $legacyName = switch ($major) { '11.0' { 'Microsoft Office 2003' } '10.0' { 'Microsoft Office XP (2002)' } '9.0' { 'Microsoft Office 2000' } '8.0' { 'Microsoft Office 97' } default { "Microsoft Office ($major)" } } Add-JdsOfficeCandidate -List $candidates -DisplayName $legacyName -Version $major -InstallType 'Legacy' -Source "InstallRoot:$major" } } } $all = @($candidates | Sort-Object Score -Descending) $best = $all | Select-Object -First 1 $found = [bool]$best $display = '' $year = $null $edition = '' if ($best) { $display = $best.DisplayName $year = $best.Year $edition = $best.Edition if (-not $edition) { $edition = Get-JdsOfficeEditionFromName $display } if ($year -and $display -notmatch "$year" -and $year -ne '365') { if ($display -notmatch 'Microsoft Office') { $display = "Microsoft Office $display $year" } elseif ($display -notmatch '\d{4}') { $display = "$display $year" } } if ($year -eq '365' -and $display -notmatch '365') { $display = if ($display) { "$display (Microsoft 365)" } else { 'Microsoft 365' } } } [pscustomobject]@{ Found = $found DisplayName = $display Year = $year Edition = $edition Version = if ($best) { $best.Version } else { '' } InstallType = if ($best) { $best.InstallType } else { '' } Channel = if ($best) { $best.Channel } else { '' } ClientFolder = if ($best) { $best.ClientFolder } else { '' } Components = @($all | Select-Object DisplayName, Version, InstallType, Source -Unique) } } function Format-JdsInstallDate([string]$Raw) { if (-not $Raw) { return '—' } $digits = ($Raw -replace '\D', '') if ($digits.Length -ge 8) { try { return [datetime]::ParseExact($digits.Substring(0, 8), 'yyyyMMdd', $null).ToString('dd/MM/yyyy') } catch { } } return $Raw } function Test-JdsAppExcludedFromBloat { param([string]$Name, [string]$Publisher) $n = $Name.ToLowerInvariant() if ($n -match 'visual c\+\+|\.net |windows sdk|office |microsoft 365|microsoft edge|defender|directx|powershell|nvidia|amd software|intel\(r\)|realtek|microsoft visual studio|sql server|azure|teams.*work|zoom|slack|chrome|firefox|7-zip|vlc|adobe creative') { return $true } return $false } function Get-JdsAppBloatInfo { param([string]$Name, [string]$Publisher) if (Test-JdsAppExcludedFromBloat -Name $Name -Publisher $Publisher) { return [pscustomobject]@{ Score = 0; Label = '' } } $n = $Name.ToLowerInvariant() $p = if ($Publisher) { $Publisher.ToLowerInvariant() } else { '' } $text = "$n $p" $high = @( 'candy crush', 'bubble witch', 'royal match', 'solitaire & casual games', 'microsoft solitaire', 'mahjong', 'disney', 'tiktok', 'instagram', 'netflix', 'spotify', 'facebook', 'mcafee', 'norton', 'webadvisor', 'wildtangent', 'wildgames', 'ask toolbar', 'babylon', 'conduit search', 'clipchamp', 'mixed reality portal', 'cortana', 'microsoft news', 'msn weather', 'get help', 'get started', 'microsoft tips', 'phone link', 'your phone', 'skype', 'teams machine-wide installer', 'xbox', 'groove music', 'movies & tv', 'zune', 'hp jumpstart', 'hp documentation', 'hp audio switch', 'hp connection optimizer', 'hp eprint', 'dell digital delivery', 'dell supportassist', 'alienware digital delivery', 'dell customer connect', 'lenovo vantage', 'lenovo companion', 'lenovo utility', 'lenovo now', 'asus giftbox', 'asus live update', 'asus optimization', 'asus software manager', 'acer jumpstart', 'acer collection', 'acer care center', 'acer registration', 'dragon center', 'msi center', 'killer performance', 'killer control center', 'samsung update', 'lg update', 'sony vaio', 'toshiba service station', 'expressvpn', 'dashlane', 'lastpass', 'avast', 'avg ', 'kaspersky', 'ccleaner', 'driver booster', 'driverpack', 'pc helpsoft', 'pc accelerate', 'bonzi buddy', 'search protect', 'yahoo powered', 'amazon assistant' ) $medium = @( 'trial', 'essai', 'free trial', 'version d''évaluation', 'java auto updater', 'silverlight', 'bonjour', 'epic games', 'riot client', 'ubisoft connect', 'ea app', 'origin', 'dropbox promotion', 'evernote', 'onenote for windows', 'adobe acrobat reader.*web', 'acrobat distiller', 'intel driver & support', 'intel® driver', 'intel driver and support', 'realtek audio console', 'waves maxxaudio' ) foreach ($pat in $high) { if ($pat -match '[\.\*\(\)\|]') { if ($text -match $pat) { return [pscustomobject]@{ Score = 2; Label = 'Bloatware' } } } elseif ($text.Contains($pat)) { return [pscustomobject]@{ Score = 2; Label = 'Bloatware' } } } foreach ($pat in $medium) { if ($pat -match '[\.\*\(\)\|]') { if ($text -match $pat) { return [pscustomobject]@{ Score = 1; Label = 'À vérifier' } } } elseif ($text.Contains($pat)) { return [pscustomobject]@{ Score = 1; Label = 'À vérifier' } } } if ($p -match 'wildtangent|mcafee|norton|ask\.com|babylon') { return [pscustomobject]@{ Score = 2; Label = 'Bloatware' } } [pscustomobject]@{ Score = 0; Label = '' } } function Get-JdsApps { $rows = New-Object System.Collections.Generic.List[object] $targets = @( @{ Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'; Scope = 'Machine 64' }, @{ Path = 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'; Scope = 'Machine 32' }, @{ Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'; Scope = 'Utilisateur' } ) foreach ($t in $targets) { Get-ItemProperty -Path $t.Path -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -and $_.SystemComponent -ne 1 } | ForEach-Object { $sizeMb = $null if ($_.EstimatedSize) { $sizeMb = [math]::Round($_.EstimatedSize / 1024, 1) } $bloat = Get-JdsAppBloatInfo -Name $_.DisplayName -Publisher $_.Publisher $rows.Add([pscustomobject]@{ IsSelected = $false Name = [string]$_.DisplayName Version = [string]$_.DisplayVersion Publisher = [string]$_.Publisher InstallDate = Format-JdsInstallDate ([string]$_.InstallDate) SizeMB = $sizeMb SizeDisplay = if ($sizeMb) { "$sizeMb Mo" } else { '—' } BloatScore = $bloat.Score BloatLabel = $bloat.Label Scope = $t.Scope UninstallString = [string]$_.UninstallString QuietUninstallString = [string]$_.QuietUninstallString }) | Out-Null } } $rows | Sort-Object Name -Unique } function Format-JdsWmiDate([string]$WmiDate) { if (-not $WmiDate) { return '' } if ($WmiDate -match '^(\d{4})(\d{2})(\d{2})') { return "$($Matches[3])/$($Matches[2])/$($Matches[1])" } return $WmiDate } function Get-JdsRamTypeName([int]$Type) { switch ($Type) { 20 { return 'DDR' } 21 { return 'DDR2' } 24 { return 'DDR3' } 26 { return 'DDR4' } 34 { return 'DDR5' } default { return '' } } } function Test-JdsGpuVisible([string]$Name) { if (-not $Name) { return $false } if ($Name -match '(?i)Microsoft Basic Display|Microsoft Remote Display|Hyper-V Video|VirtualBox|VMware SVGA|VMware Virtual|Parsec Virtual|Indirect Display|Mirror Driver|TeamViewer Virtual|RDP Display|Remote Desktop|GameViewer Virtual|Oray Display|Virtual Display') { return $false } return $true } function Test-JdsPnPNoise([string]$Name) { if (-not $Name) { return $true } if ($Name -match '(?i)ACPI|USB Hub|Generic USB|Root Hub|HID-compliant|Standard NVM|Composite Device|Bluetooth|High Definition Audio|Wireless|Wi-Fi|Ethernet Controller|System Firmware|UMBus|System Management|PCI standard|Trusted Platform|High precision event timer|Direct memory access|Programmable interrupt|System timer|Numeric data processor|Volume Manager|Disk drive|Generic Monitor|Keyboard|Mouse|Biometric|Smart card|Cellular|GPS|Sensor|Portable Device|Print Queue|Scanner|Fax|WAN Miniport|NDIS|Virtual|Hyper-V|VMware|Remote Desktop|Basic Display|Generic PnP|Toaster|Enumerator|Charge Arbitration|GS Wavetable|USB Input Device|USB Mass Storage|USB Composite|USB Root Hub|Standard SATA|Standard NVM Express|Standard disk|PCI-to-PCI Bridge|Audio Endpoint|Media Control|Streaming|Sound|Webcam|Camera|Touch Pad|Fingerprint|Card Reader|Docking|Thunderbolt.*Router|Intel\(R\) Wi-Fi|Intel\(R\) Ethernet|Realtek.*Audio|Realtek.*USB|NVIDIA Virtual Audio|AMD Audio|AMD USB|DisplayPort|HDMI|Monitor|Panel|LCD|LED|Graphics Command Center|Radeon Software|GeForce Experience|Intel\(R\) Management Engine Interface|Intel\(R\) Serial IO|Intel\(R\) Gaussian|Intel\(R\) HID|Intel\(R\) Wireless|Intel\(R\) Bluetooth|AMD PSP|AMD SMBus|AMD GPIO|AMD PCI|AMD USB|AMD Ryzen Master|Power Engine|Thermal Controller|Fan Controller|Battery|ACPI Processor|Microsoft ACPI|Microsoft UEFI|System CMOS|System board|Motherboard resources|Microsoft System|Plug and Play|Software Device|Software components|Extension INF|Firmware|Driver Enumeration|Device Firmware|System devices') { return $true } return $false } function Get-JdsChipsetName { $patterns = @( '(?i)\bchipset\b', '(?i)Series Chipset', '(?i)Platform Controller Hub', '(?i)LPC Controller', '(?i)PCI Express Root Complex', '(?i)FCH SMBus Controller', '(?i)SM Bus Controller.*(Intel|AMD|Series)' ) $devices = @(Get-CimInstance -ClassName Win32_PnPEntity -ErrorAction SilentlyContinue | Where-Object { $_.Name -and -not (Test-JdsPnPNoise $_.Name) }) foreach ($pat in $patterns) { $hit = @($devices | Where-Object { $_.Name -match $pat } | Select-Object -First 1) if ($hit) { return $hit.Name.Trim() } } return '' } function Get-JdsHardware { $cpu = Get-CimInstance -ClassName Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1 $baseboard = Get-CimInstance -ClassName Win32_BaseBoard -ErrorAction SilentlyContinue $bios = Get-CimInstance -ClassName Win32_BIOS -ErrorAction SilentlyContinue $ram = @(Get-CimInstance -ClassName Win32_PhysicalMemory -ErrorAction SilentlyContinue) $ramTotalGb = 0.0 if ($ram.Count -gt 0) { $ramTotalGb = [math]::Round(($ram | Measure-Object -Property Capacity -Sum).Sum / 1GB, 1) } $ramTypes = @($ram | ForEach-Object { Get-JdsRamTypeName ([int]$_.SMBIOSMemoryType) } | Where-Object { $_ } | Select-Object -Unique) $ramType = if ($ramTypes.Count -eq 1) { $ramTypes[0] } elseif ($ramTypes.Count -gt 1) { ($ramTypes -join '/') } else { '' } $ramSpeeds = @($ram | ForEach-Object { [int]$_.Speed } | Where-Object { $_ -gt 0 } | Select-Object -Unique) $ramSpeed = if ($ramSpeeds.Count -gt 0) { ($ramSpeeds | Measure-Object -Maximum).Maximum } else { 0 } $ramSticks = @($ram | ForEach-Object { $gb = [math]::Round($_.Capacity / 1GB, 0) $type = Get-JdsRamTypeName ([int]$_.SMBIOSMemoryType) $spd = if ([int]$_.Speed -gt 0) { " @ $($_.Speed) MHz" } else { '' } $mfr = ($_.Manufacturer -replace '\s+', ' ').Trim() $part = ($_.PartNumber -replace '\s+', ' ').Trim() $label = if ($part) { $part } elseif ($mfr) { $mfr } else { 'Barrette' } $typeTxt = if ($type) { " $type" } else { '' } " • $gb Go$typeTxt$spd — $label" }) $gpuRaw = @(Get-CimInstance -ClassName Win32_VideoController -ErrorAction SilentlyContinue | Where-Object { Test-JdsGpuVisible $_.Name }) $gpus = @($gpuRaw | ForEach-Object { $drvVer = [string]$_.DriverVersion if ($drvVer -match '^(\d+\.\d+\.\d+\.\d+)$') { $drvVer = $Matches[1] } [pscustomobject]@{ Name = ([string]$_.Name).Trim() DriverVersion = $drvVer DriverDate = Format-JdsWmiDate ([string]$_.DriverDate) } }) $mbManufacturer = if ($baseboard -and $baseboard.Manufacturer) { $baseboard.Manufacturer.Trim() } else { '' } $mbModel = if ($baseboard -and $baseboard.Product) { $baseboard.Product.Trim() } else { '' } if ($mbModel -match '(?i)^(base board|default string|to be filled|not available|system product)') { $mbModel = '' } $biosVersion = if ($bios) { [string]$bios.SMBIOSBIOSVersion } else { '' } if (-not $biosVersion -and $bios) { $biosVersion = [string]$bios.Version } $biosVendor = if ($bios) { [string]$bios.Manufacturer } else { '' } $biosDate = if ($bios) { Format-JdsWmiDate ([string]$bios.ReleaseDate) } else { '' } $chipset = Get-JdsChipsetName $cpuName = if ($cpu) { ([string]$cpu.Name).Trim() -replace '\s+', ' ' } else { '' } $cpuCores = if ($cpu) { [int]$cpu.NumberOfCores } else { 0 } $cpuLogical = if ($cpu) { [int]$cpu.NumberOfLogicalProcessors } else { 0 } $cpuMhz = if ($cpu) { [int]$cpu.MaxClockSpeed } else { 0 } $ramSpeedLabel = if ($ramType -and $ramSpeed -gt 0) { "$ramType-$ramSpeed" } elseif ($ramType) { $ramType } elseif ($ramSpeed -gt 0) { "$ramSpeed MHz" } else { '' } [pscustomobject]@{ CpuName = $cpuName CpuCores = $cpuCores CpuLogical = $cpuLogical CpuMaxMHz = $cpuMhz RamGB = $ramTotalGb RamType = $ramType RamSpeedMHz = $ramSpeed RamSpeedLabel = $ramSpeedLabel RamSticks = $ramSticks MotherboardManufacturer = $mbManufacturer MotherboardModel = $mbModel BiosVersion = $biosVersion BiosVendor = $biosVendor BiosDate = $biosDate Chipset = $chipset Gpus = $gpus # rétrocompat dashboard / ancien code Manufacturer = $mbManufacturer Model = $mbModel BiosSerial = if ($bios) { [string]$bios.SerialNumber } else { '' } Serial = if ($bios) { [string]$bios.SerialNumber } else { '' } } } function New-JdsFinding { param( [string]$Level, [string]$Title, [string]$Cause, [string]$Effect, [string]$Action ) @" [$Level] $Title Cause : $Cause Effet : $Effect Action : $Action "@ } function Format-JdsMac([string]$Mac) { if (-not $Mac) { return '' } $clean = ($Mac -replace '[-:. ]', '').ToUpper() if ($clean.Length -ne 12) { return $Mac } $pairs = for ($i = 0; $i -lt 12; $i += 2) { $clean.Substring($i, 2) } return ($pairs -join ':') } function Get-JdsNetshConfig { param([string]$Alias) if (-not $Alias) { return '' } $safe = $Alias -replace '"', '' return (netsh interface ipv4 show config name="$safe" 2>&1 | Out-String) } function Get-JdsIpModeFromNetsh([string]$Text) { if ($Text -match '(?i)DHCP enabled:\s+Yes|DHCP activé\s*:\s+Oui') { return 'DHCP (automatique)' } if ($Text -match '(?i)DHCP enabled:\s+No|DHCP activé\s*:\s+Non') { return 'IP fixe (manuel)' } return 'Inconnu' } function Get-JdsDnsModeFromNetsh([string]$Text) { if ($Text -match '(?i)Statically Configured DNS Servers|Serveurs DNS configurés statiquement') { return 'Manuel' } if ($Text -match '(?i)DNS servers configured through DHCP:\s+None|Serveurs DNS configurés via DHCP\s*:\s*Aucun') { if ($Text -match '(?i)DNS servers|Serveurs DNS') { return 'Manuel' } return 'Aucun' } if ($Text -match '(?i)DNS servers configured through DHCP:|Serveurs DNS configurés via DHCP\s*:') { return 'Automatique (DHCP)' } return 'Inconnu' } function Get-JdsDnsFromNetsh([string]$Text) { $lines = $Text -split "`r?`n" $dns = New-Object System.Collections.Generic.List[string] $inStatic = $false foreach ($line in $lines) { if ($line -match '(?i)Statically Configured DNS|Serveurs DNS configurés statiquement') { $inStatic = $true; continue } if ($line -match '(?i)DNS servers configured through DHCP:|Serveurs DNS configurés via DHCP\s*:') { $inStatic = $false $v = ($line -split ':', 2)[1].Trim() if ($v -and $v -notmatch '^(?i)(none|aucun)$') { $dns.Add($v) } continue } if ($inStatic -and $line -match '^\s+(\d+\.\d+\.\d+\.\d+)') { $dns.Add($Matches[1]) } } return ($dns -join ', ') } function Get-JdsNetworkCategoryLabel($Category) { switch ([string]$Category) { '0' { 'Public' } 'Public' { 'Public' } '1' { 'Privé' } 'Private' { 'Privé' } '2' { 'Domaine' } 'DomainAuthenticated' { 'Domaine' } default { [string]$Category } } } function Get-JdsNetwork { $cs = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue $candidateList = @() try { $configs = @(Get-NetIPConfiguration -ErrorAction Stop | Where-Object { $_.NetAdapter }) foreach ($c in $configs) { $na = $c.NetAdapter $alias = [string]$na.InterfaceAlias $desc = [string]$na.InterfaceDescription $idx = [int]$na.InterfaceIndex $linkStatus = [string]$na.Status $isUp = ($linkStatus -eq 'Up') $ipv4 = @($c.IPv4Address | ForEach-Object { $_.IPAddress } | Where-Object { $_ }) -join ', ' $ipv6 = @($c.IPv6Address | ForEach-Object { $_.IPAddress } | Where-Object { $_ -and $_ -notmatch '^fe80' }) -join ', ' $gw4 = @($c.IPv4DefaultGateway | ForEach-Object { $_.NextHop } | Where-Object { $_ }) -join ', ' $gw6 = @($c.IPv6DefaultGateway | ForEach-Object { $_.NextHop } | Where-Object { $_ }) -join ', ' $gateway = @($gw4, $gw6 | Where-Object { $_ }) -join ' · ' $netsh = if ($isUp) { Get-JdsNetshConfig $alias } else { '' } $ipMode = if ($netsh) { Get-JdsIpModeFromNetsh $netsh } else { '—' } $dnsMode = if ($netsh) { Get-JdsDnsModeFromNetsh $netsh } else { '—' } $dns = if ($netsh) { Get-JdsDnsFromNetsh $netsh } else { '' } if (-not $dns) { $dns = @($c.DNSServer.ServerAddresses | Where-Object { $_ }) -join ', ' } $category = 'Inconnu' try { $prof = Get-NetConnectionProfile -InterfaceIndex $idx -ErrorAction Stop | Select-Object -First 1 $category = Get-JdsNetworkCategoryLabel $prof.NetworkCategory } catch { } $isEth = ($alias -match '(?i)Ethernet') -or ($desc -match '(?i)Ethernet|Realtek.*(LAN|Gbe)|Intel.*(I219|I225|LAN|Ethernet)' -and $desc -notmatch '(?i)Wi-?Fi|Wireless') $isWifi = ($alias -match '(?i)Wi-?Fi|WLAN') -or ($desc -match '(?i)Wireless|Wi-?Fi|802\.11') $isVirtual = $desc -match '(?i)Virtual|Hyper-V|VMware|VPN|TAP|TUN|vEthernet|Wintun|Tailscale|ZeroTier|Loopback' $mediaType = if ($isWifi) { 'Wi-Fi' } elseif ($isEth) { 'Ethernet' } else { 'Réseau' } $statusLabel = if ($isUp) { 'Connecté' } else { 'Déconnecté' } $mac = Format-JdsMac ([string]$na.MacAddress) $candidateList += [pscustomobject]@{ Alias = $alias Description = $desc MediaType = $mediaType Status = $linkStatus StatusLabel = $statusLabel IPv4 = $ipv4 IPv6 = $ipv6 Gateway = $gateway Dns = $dns Mac = $mac IpMode = $ipMode DnsMode = $dnsMode Category = $category IsEthernet = [bool]$isEth IsWifi = [bool]$isWifi IsVirtual = [bool]$isVirtual IsUp = $isUp HasGateway = [bool]$gateway HasIPv4 = [bool]$ipv4 } } } catch { Get-CimInstance Win32_NetworkAdapterConfiguration -Filter 'IPEnabled=TRUE' -ErrorAction SilentlyContinue | ForEach-Object { $dhcp = if ($_.DHCPEnabled) { 'DHCP (automatique)' } else { 'IP fixe (manuel)' } $ipv4 = @($_.IPAddress | Where-Object { $_ -match '^\d+\.' }) -join ', ' $ipv6 = @($_.IPAddress | Where-Object { $_ -match ':' }) -join ', ' $candidateList += [pscustomobject]@{ Alias = $_.Description Description = $_.Description MediaType = 'Réseau' Status = 'Up' StatusLabel = 'Connecté' IPv4 = $ipv4 IPv6 = $ipv6 Gateway = @($_.DefaultIPGateway) -join ', ' Dns = @($_.DNSServerSearchOrder) -join ', ' Mac = Format-JdsMac ([string]$_.MACAddress) IpMode = $dhcp DnsMode = if ($_.DHCPEnabled) { 'Automatique (DHCP)' } else { 'Manuel' } Category = 'Inconnu' IsEthernet = $true IsWifi = $false IsVirtual = $false IsUp = $true HasGateway = [bool]$_.DefaultIPGateway HasIPv4 = [bool]$ipv4 } } } $real = @($candidateList | Where-Object { -not $_.IsVirtual }) if ($real.Count -eq 0) { $real = @($candidateList) } $primary = @($real | Where-Object { $_.IsUp -and $_.HasGateway } | Sort-Object { if ($_.IsEthernet) { 0 } elseif ($_.IsWifi) { 1 } else { 2 } } | Select-Object -First 1) if (-not $primary) { $primary = @($real | Where-Object { $_.IsUp -and $_.HasIPv4 } | Sort-Object { if ($_.IsEthernet) { 0 } elseif ($_.IsWifi) { 1 } else { 2 } } | Select-Object -First 1) } if (-not $primary) { $primary = @($real | Where-Object { $_.IsUp } | Sort-Object { if ($_.IsEthernet) { 0 } elseif ($_.IsWifi) { 1 } else { 2 } } | Select-Object -First 1) } if (-not $primary) { $primary = $real | Select-Object -First 1 } $ssid = '' try { $wlan = netsh wlan show interfaces 2>$null | Out-String if ($wlan -match '(?m)^\s+SSID\s+:\s+(.+)$') { $ssid = $Matches[1].Trim() } } catch { } $mapped = @() Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=4' -ErrorAction SilentlyContinue | ForEach-Object { $mapped += [pscustomobject]@{ Letter = $_.DeviceId; Remote = $_.ProviderName; Label = $_.VolumeName } } try { Get-SmbMapping -ErrorAction Stop | ForEach-Object { $letter = if ($_.LocalPath) { $_.LocalPath } else { $_.RemotePath } if ($mapped.Remote -notcontains $_.RemotePath) { $mapped += [pscustomobject]@{ Letter = $letter; Remote = $_.RemotePath; Label = $_.DisplayName } } } } catch { } $proxyHttp = '' try { $proxyHttp = (netsh winhttp show proxy 2>&1 | Out-String).Trim() } catch { } $proxyInet = '' try { $inet = Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -ErrorAction SilentlyContinue if ($inet.ProxyEnable -eq 1) { $proxyInet = $inet.ProxyServer } } catch { } $allAdapters = @($candidateList) $otherCount = [math]::Max(0, $allAdapters.Count - 1) [pscustomobject]@{ Primary = $primary OtherCount = $otherCount OtherVirtual = @($allAdapters | Where-Object { $_.IsVirtual -eq $true }).Count Ssid = $ssid PartOfDomain = [bool]$cs.PartOfDomain Domain = $cs.Domain MappedDrives = @($mapped) ProxyWinHttp = $proxyHttp ProxyInet = $proxyInet Adapters = $allAdapters } } function Get-JdsNetworkTechNotes($net) { $notes = New-Object System.Collections.Generic.List[string] $p = $net.Primary if (-not $p) { $notes.Add('Aucune connexion IPv4 active détectée.') return $notes } if ($p.IpMode -match 'fixe|manuel') { $notes.Add('IP FIXE — noter IP/passerelle/masque avant reset TCP/IP, winsock ou changement de carte.') } else { $notes.Add('IP en DHCP — reset réseau en général OK (release/renew, winsock).') } if ($p.DnsMode -eq 'Manuel') { $notes.Add('DNS MANUEL — documenter les serveurs avant flush DNS ou reset pile IP.') } elseif ($p.DnsMode -match 'Automatique') { $notes.Add('DNS via DHCP — flush DNS sans risque majeur.') } switch ($p.Category) { 'Public' { $notes.Add('Profil PUBLIC — partage et découverte limités (normal).') } 'Privé' { $notes.Add('Profil PRIVÉ — découverte réseau autorisée (imprimantes, partages locaux).') } 'Domaine' { $notes.Add('Profil DOMAINE — GPO et ressources AD ; éviter reset réseau sans raison.') } } if ($net.PartOfDomain) { $notes.Add("Joint au domaine $($net.Domain) — ne pas quitter le domaine ; GPO peut imposer IP/DNS.") } else { $notes.Add("Workgroup / $($net.Domain) — pas de GPO domaine ; plus de liberté sur la pile réseau.") } if ($net.MappedDrives.Count -gt 0) { $notes.Add("$($net.MappedDrives.Count) lecteur(s) réseau mappé(s) — ne pas déconnecter sans vérifier logiciels métier.") } if ($net.ProxyInet -or ($net.ProxyWinHttp -and $net.ProxyWinHttp -notmatch 'direct|directe')) { $notes.Add('Proxy configuré — reset proxy WinHTTP peut casser Internet métier.') } if ($net.OtherVirtual -gt 0) { $notes.Add("$($net.OtherVirtual) carte(s) virtuelle(s) (VPN/Hyper-V) ignorée(s) dans la synthèse.") } if ($net.OtherCount -gt 0) { $notes.Add("$($net.OtherCount) autre(s) carte(s) active(s) non affichée(s) — voir Gestionnaire de périphériques si besoin.") } return $notes } function Get-JdsDisks { param([switch]$Quick) $vols = @(Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -ErrorAction SilentlyContinue) $volumes = foreach ($v in $vols) { $size = [math]::Round($v.Size / 1GB, 1) $free = [math]::Round($v.FreeSpace / 1GB, 1) $pct = if ($v.Size -gt 0) { [math]::Round(100 * $v.FreeSpace / $v.Size, 0) } else { 0 } [pscustomobject]@{ DeviceId = $v.DeviceID Label = $v.VolumeName FileSystem = $v.FileSystem SizeGB = $size FreeGB = $free FreePct = $pct } } $bitlocker = @() $physical = @() if (-not $Quick) { try { $bitlocker = @(Get-BitLockerVolume -ErrorAction Stop | ForEach-Object { [pscustomobject]@{ Mount = $_.MountPoint Protect = [string]$_.ProtectionStatus Volume = [string]$_.VolumeStatus Method = (@($_.KeyProtector | ForEach-Object { $_.KeyProtectorType }) -join ', ') EncPct = $_.EncryptionPercentage } }) } catch { } try { $physical = @(Get-PhysicalDisk -ErrorAction Stop | ForEach-Object { [pscustomobject]@{ Friendly = $_.FriendlyName Media = [string]$_.MediaType Health = [string]$_.HealthStatus SizeGB = [math]::Round($_.Size / 1GB, 0) Bus = [string]$_.BusType } }) } catch { } } [pscustomobject]@{ Volumes = $volumes BitLocker = $bitlocker Physical = $physical } } function Format-JdsUserLastLogon($Value) { if (-not $Value) { return 'Jamais' } try { $dt = [datetime]$Value if ($dt.Year -lt 1980) { return 'Jamais' } return $dt.ToString('dd/MM/yyyy HH:mm') } catch { return 'Jamais' } } function Get-JdsUserAccountKind($User) { $src = [string]$User.PrincipalSource switch ($src) { 'MicrosoftAccount' { return 'Compte Microsoft' } 'AzureAD' { return 'Compte Microsoft / Entra ID' } 'Local' { return 'Compte local' } default { if ($User.Name -match '@' -or [string]$User.Description -match '@') { return 'Compte Microsoft' } return 'Compte local' } } } function Test-JdsSystemUserAccount { param($User) $name = [string]$User.Name if (-not $name) { return $true } $lower = $name.ToLowerInvariant() if ($name -match '\$$') { return $true } $exactBlock = @( 'WDAGUtilityAccount', 'DefaultAccount', 'DefaultUser0', 'Guest', 'DevToolsUser', 'sshd' ) if ($exactBlock -contains $name) { return $true } if ($lower -match 'sandbox|wdagutility|defaultaccount|defaultuser\d*|healthmailbox|^_apt|systemprofile|localservice|networkservice|codexsandbox|mslda|windowssandbox|wsl|docker|kube|vagrant|testuser_temp|temporal|cli-user|cloudbase') { return $true } if ($lower -match '^(admin|user|test|demo)\d{3,}$') { return $true } try { $sid = $null if ($User.SID) { $sid = $User.SID.Value } if ($sid) { $rid = [int]($sid -split '-')[-1] if ($rid -in 501, 503, 504, 506, 507, 508) { return $true } } } catch { } $desc = [string]$User.Description if ($desc -match '(?i)built-in|compte intégré|system account|service account|windows sandbox|defender sandbox') { return $true } return $false } function Test-JdsUserIsAdmin { param([string]$UserName, [string[]]$AdminMembers) foreach ($m in $AdminMembers) { $short = if ($m -match '\\([^\\]+)$') { $Matches[1] } else { $m } if ($short -eq $UserName) { return $true } } return $false } function Get-JdsUsers { $localRaw = @() try { $localRaw = @(Get-LocalUser -ErrorAction Stop) } catch { } $admins = @() try { $admins = @(Get-LocalGroupMember -Group 'Administrateurs' -ErrorAction Stop | ForEach-Object { $_.Name }) } catch { try { $admins = @(Get-LocalGroupMember -Group 'Administrators' -ErrorAction Stop | ForEach-Object { $_.Name }) } catch { } } $filtered = 0 $accounts = @() foreach ($u in $localRaw) { if (Test-JdsSystemUserAccount $u) { $filtered++ continue } $isAdmin = Test-JdsUserIsAdmin -UserName $u.Name -AdminMembers $admins $accounts += [pscustomobject]@{ Name = $u.Name Status = if ($u.Enabled) { 'Actif' } else { 'Désactivé' } Enabled = [bool]$u.Enabled Type = if ($isAdmin) { 'Admin' } else { 'Standard' } AccountKind = Get-JdsUserAccountKind $u LastLogon = Format-JdsUserLastLogon $u.LastLogon LastLogonRaw = $u.LastLogon IsCurrent = ($u.Name -eq $env:USERNAME) } } $accounts = @($accounts | Sort-Object @{ Expression = { - [int]$_.Enabled }; Descending = $false }, @{ Expression = { - [int]$_.IsCurrent }; Descending = $false }, Name) $cs = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue [pscustomobject]@{ Accounts = $accounts FilteredCount = $filtered CurrentUser = "$env:USERDOMAIN\$env:USERNAME" LoggedOn = $cs.UserName Domain = $cs.Domain PartOfDomain = [bool]$cs.PartOfDomain Workgroup = $cs.Workgroup Admins = $admins Local = $accounts } } function Get-JdsWuRegistryDate { param([string]$Path, [string]$Name) if (-not (Test-Path -LiteralPath $Path)) { return $null } try { $v = (Get-ItemProperty -LiteralPath $Path -Name $Name -ErrorAction Stop).$Name if ($v -is [datetime]) { return $v } if ([string]$v -match '^\d{4}') { return [datetime]$v } } catch { } return $null } function Get-JdsUpdates { $os = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue $svc = Get-Service -Name wuauserv -ErrorAction SilentlyContinue $svcStatus = 'Indisponible' if ($svc) { $st = switch ([string]$svc.Status) { 'Running' { 'En cours d''exécution' } 'Stopped' { 'Arrêté' } 'Paused' { 'En pause' } default { [string]$svc.Status } } $start = switch ([string]$svc.StartType) { 'Automatic' { 'Automatique' } 'Manual' { 'Manuel' } 'Disabled' { 'Désactivé' } default { [string]$svc.StartType } } $svcStatus = "$st · démarrage $start" } $detectPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\Results\Detect' $installPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\Results\Install' $lastSearch = Get-JdsWuRegistryDate -Path $detectPath -Name 'LastSuccessTime' $lastInstall = Get-JdsWuRegistryDate -Path $installPath -Name 'LastSuccessTime' $allHotfixes = @(Get-HotFix -ErrorAction SilentlyContinue) $hotfixes = @($allHotfixes | Sort-Object { if ($_.InstalledOn) { $_.InstalledOn } else { [datetime]::MinValue } } -Descending | Select-Object -First 5 | ForEach-Object { $dt = if ($_.InstalledOn) { $_.InstalledOn.ToString('dd/MM/yyyy') } else { '—' } $title = [string]$_.Description if ($title.Length -gt 48) { $title = $title.Substring(0, 45) + '…' } [pscustomobject]@{ Id = $_.HotFixID Date = $dt Title = $title } }) [pscustomobject]@{ ServiceStatus = $svcStatus LastSearch = if ($lastSearch) { $lastSearch.ToString('dd/MM/yyyy HH:mm') } else { '—' } LastInstall = if ($lastInstall) { $lastInstall.ToString('dd/MM/yyyy HH:mm') } else { '—' } LastBoot = if ($os) { $os.LastBootUpTime } else { $null } Hotfixes = $hotfixes HotfixCount = $allHotfixes.Count } } # --------------------------------------------------------------------------- # Formatage texte # --------------------------------------------------------------------------- function Format-JdsWindowsText($os, $lic) { $role = if ($os.PartOfDomain) { "Domaine : $($os.Domain)" } else { "Groupe de travail : $($os.Domain)" } $grace = if ($lic.RemainingGrace) { $lic.RemainingGrace } else { '—' } $channel = if ($lic.Channel) { $lic.Channel } else { '—' } $key = if ($lic.PartialKey) { $lic.PartialKey } else { '—' } @" Ordinateur : $($os.ComputerName) Utilisateur : $($os.User) $role Fabricant / modèle: $($os.Manufacturer) / $($os.Model) Système : $($os.Caption) Édition : $($os.EditionId) Version : $($os.DisplayVersion) Build : $($os.Build) Architecture : $($os.Arch) Type d'install. : $($os.InstallType) Installé le : $($os.InstallDate) Dernier boot : $($os.LastBoot) Uptime : $($os.Uptime) --- Licence Windows --- Produit : $($lic.Name) Statut : $($lic.Status) Canal (clé) : $channel Description : $($lic.Description) Clé (5 derniers) : $key Grace restante : $grace "@ } function Format-JdsOfficeText($off) { if (-not $off.Found) { return 'Aucun Microsoft Office / Microsoft 365 détecté sur ce poste.' } $yearTxt = if ($off.Year -eq '365') { 'Microsoft 365 (abonnement)' } elseif ($off.Year) { [string]$off.Year } else { '—' } $lines = New-Object System.Collections.Generic.List[string] [void]$lines.Add($off.DisplayName) [void]$lines.Add('') [void]$lines.Add("Édition : $(if ($off.Edition) { $off.Edition } else { '—' })") [void]$lines.Add("Année / suite : $yearTxt") [void]$lines.Add("Version : $(if ($off.Version) { $off.Version } else { '—' })") [void]$lines.Add("Type d'installation : $(if ($off.InstallType) { $off.InstallType } else { '—' })") if ($off.Channel) { [void]$lines.Add("Canal de MAJ : $($off.Channel)") } if ($off.ClientFolder) { [void]$lines.Add("Dossier client : $($off.ClientFolder)") } if ($off.Components -and $off.Components.Count -gt 1) { [void]$lines.Add('') [void]$lines.Add('--- Autres détections ---') foreach ($c in $off.Components | Select-Object -First 12) { $v = if ($c.Version) { " ($($c.Version))" } else { '' } [void]$lines.Add(('- {0}{1} [{2}]' -f $c.DisplayName, $v, $c.InstallType)) } } $lines -join "`r`n" } function Format-JdsHardwareText($hw) { if (-not $hw) { return 'Matériel non disponible — actualisez depuis l''accueil.' } $lines = New-Object System.Collections.Generic.List[string] [void]$lines.Add('── PROCESSEUR ─────────────────────────────────────') if ($hw.CpuName) { [void]$lines.Add(" $($hw.CpuName)") if ($hw.CpuCores -or $hw.CpuLogical) { $cpuDetail = @() if ($hw.CpuCores) { $cpuDetail += "$($hw.CpuCores) cœur(s)" } if ($hw.CpuLogical) { $cpuDetail += "$($hw.CpuLogical) thread(s)" } if ($hw.CpuMaxMHz) { $cpuDetail += "$($hw.CpuMaxMHz) MHz max" } [void]$lines.Add(" $($cpuDetail -join ' · ')") } } else { [void]$lines.Add(' —') } [void]$lines.Add('') [void]$lines.Add('── MÉMOIRE (RAM) ──────────────────────────────────') $ramHead = if ($hw.RamGB) { "$($hw.RamGB) Go total" } else { 'Capacité inconnue' } if ($hw.RamSpeedLabel) { $ramHead += " — $($hw.RamSpeedLabel)" } [void]$lines.Add(" $ramHead") if ($hw.RamSticks -and @($hw.RamSticks).Count -gt 0) { foreach ($stick in @($hw.RamSticks)) { [void]$lines.Add($stick) } } else { [void]$lines.Add(' • Détail des barrettes indisponible') } [void]$lines.Add('') [void]$lines.Add('── CARTE MÈRE ───────────────────────────────────') [void]$lines.Add(" Fabricant : $(if ($hw.MotherboardManufacturer) { $hw.MotherboardManufacturer } else { '—' })") [void]$lines.Add(" Modèle : $(if ($hw.MotherboardModel) { $hw.MotherboardModel } else { '—' })") $biosLine = if ($hw.BiosVersion) { $hw.BiosVersion } else { '—' } if ($hw.BiosVendor) { $biosLine += " ($($hw.BiosVendor)" } if ($hw.BiosDate) { $biosLine += if ($hw.BiosVendor) { ", $($hw.BiosDate))" } else { " ($($hw.BiosDate))" } } elseif ($hw.BiosVendor) { $biosLine += ')' } [void]$lines.Add(" BIOS : $biosLine") [void]$lines.Add('') [void]$lines.Add('── CHIPSET ──────────────────────────────────────') [void]$lines.Add(" $(if ($hw.Chipset) { $hw.Chipset } else { '—' })") [void]$lines.Add('') [void]$lines.Add('── GRAPHIQUE (GPU) ────────────────────────────────') if ($hw.Gpus -and @($hw.Gpus).Count -gt 0) { foreach ($g in @($hw.Gpus)) { $gName = if ($g.Name) { $g.Name } elseif ($g -is [string]) { $g } else { 'GPU' } [void]$lines.Add(" • $gName") if ($g.DriverVersion -or $g.DriverDate) { $drv = @() if ($g.DriverVersion) { $drv += "pilote $($g.DriverVersion)" } if ($g.DriverDate) { $drv += $g.DriverDate } [void]$lines.Add(" $(($drv -join ' · '))") } } } else { [void]$lines.Add(' —') } $lines -join "`r`n" } function Format-JdsNetworkText($net) { if (-not $net) { return 'Réseau non disponible — actualisez depuis l''accueil.' } $lines = New-Object System.Collections.Generic.List[string] $p = $net.Primary if (-not $p) { [void]$lines.Add('Aucune carte réseau active détectée.') return ($lines -join "`r`n") } [void]$lines.Add('── CONNEXION ACTIVE ───────────────────────────────') [void]$lines.Add(" Type : $($p.MediaType)") [void]$lines.Add(" Carte réseau : $($p.Alias)") if ($p.Description -and $p.Description -ne $p.Alias) { [void]$lines.Add(" Description : $($p.Description)") } [void]$lines.Add(" Statut : $($p.StatusLabel)") if ($net.Ssid) { [void]$lines.Add(" SSID Wi-Fi : $($net.Ssid)") } [void]$lines.Add(" Profil Windows : $($p.Category)") [void]$lines.Add('') [void]$lines.Add('── ADRESSES IP ───────────────────────────────────') [void]$lines.Add(" IPv4 : $(if ($p.IPv4) { $p.IPv4 } else { '—' })") [void]$lines.Add(" IPv6 : $(if ($p.IPv6) { $p.IPv6 } else { '—' })") [void]$lines.Add(" Mode IP : $($p.IpMode)") [void]$lines.Add('') [void]$lines.Add('── ROUTAGE & DNS ─────────────────────────────────') [void]$lines.Add(" Passerelle : $(if ($p.Gateway) { $p.Gateway } else { '—' })") [void]$lines.Add(" Serveurs DNS : $(if ($p.Dns) { $p.Dns } else { '—' })") [void]$lines.Add(" Mode DNS : $($p.DnsMode)") [void]$lines.Add('') [void]$lines.Add('── ADRESSE MAC ───────────────────────────────────') [void]$lines.Add(" $(if ($p.Mac) { $p.Mac } else { '—' })") if ($net.MappedDrives -and $net.MappedDrives.Count -gt 0) { [void]$lines.Add('') [void]$lines.Add('── LECTEURS RÉSEAU ───────────────────────────────') foreach ($m in $net.MappedDrives) { $lbl = if ($m.Label) { " [$($m.Label)]" } else { '' } [void]$lines.Add(" $($m.Letter) → $($m.Remote)$lbl") } } if ($net.ProxyInet -or ($net.ProxyWinHttp -and $net.ProxyWinHttp -notmatch '(?i)direct')) { [void]$lines.Add('') [void]$lines.Add('── PROXY ─────────────────────────────────────────') if ($net.ProxyInet) { [void]$lines.Add(" Navigateur : $($net.ProxyInet)") } if ($net.ProxyWinHttp) { foreach ($pl in ($net.ProxyWinHttp.Trim() -split "`r?`n")) { if ($pl.Trim()) { [void]$lines.Add(" $pl") } } } } [void]$lines.Add('') [void]$lines.Add('── NOTES TECH ────────────────────────────────────') foreach ($n in (Get-JdsNetworkTechNotes $net)) { [void]$lines.Add(" • $n") } $lines -join "`r`n" } function Format-JdsDisksText($d) { $sb = New-Object System.Text.StringBuilder [void]$sb.AppendLine('--- Volumes ---') foreach ($v in $d.Volumes) { $label = if ($v.Label) { $v.Label } else { '(sans nom)' } [void]$sb.AppendLine(('{0} {1} {2} {3} Go libres / {4} Go ({5}% libre)' -f $v.DeviceId, $label, $v.FileSystem, $v.FreeGB, $v.SizeGB, $v.FreePct)) } [void]$sb.AppendLine('') [void]$sb.AppendLine('--- Disques physiques ---') if ($d.Physical.Count) { foreach ($p in $d.Physical) { [void]$sb.AppendLine(('{0} {1} {2} {3} Go {4}' -f $p.Friendly, $p.Media, $p.Health, $p.SizeGB, $p.Bus)) } } else { [void]$sb.AppendLine('(ouvre l''onglet Disques pour charger SMART / BitLocker)') } [void]$sb.AppendLine('') [void]$sb.AppendLine('--- BitLocker ---') if ($d.BitLocker.Count) { foreach ($b in $d.BitLocker) { [void]$sb.AppendLine(('{0} protection={1} volume={2} {3}% protecteurs={4}' -f $b.Mount, $b.Protect, $b.Volume, $b.EncPct, $b.Method)) } } else { [void]$sb.AppendLine('(ouvre l''onglet Disques pour le détail BitLocker)') } $sb.ToString() } function Format-JdsUsersText($u) { if (-not $u) { return 'Utilisateurs non disponibles — ouvrez l''onglet Utilisateurs.' } $lines = New-Object System.Collections.Generic.List[string] [void]$lines.Add('── SESSION ACTUELLE ──────────────────────────────') [void]$lines.Add(" Utilisateur : $($u.CurrentUser)") if ($u.LoggedOn -and $u.LoggedOn -ne $u.CurrentUser) { [void]$lines.Add(" Session WMI : $($u.LoggedOn)") } if ($u.PartOfDomain) { [void]$lines.Add(" Domaine : $($u.Domain)") } else { [void]$lines.Add(" Groupe : $($u.Domain)") } [void]$lines.Add('') $count = if ($u.Accounts) { @($u.Accounts).Count } else { 0 } [void]$lines.Add("── COMPTES UTILISATEURS ($count) ─────────────────────") if ($count -eq 0) { [void]$lines.Add(' Aucun compte utilisateur réel détecté.') } else { [void]$lines.Add('') [void]$lines.Add(' Nom Statut Type Dernière connexion Type de compte') [void]$lines.Add(' ─────────────────── ─────────── ───────── ──────────────────── ─────────────────────') foreach ($a in @($u.Accounts)) { $name = $a.Name if ($a.IsCurrent) { $name = "* $name" } $row = (' {0,-20} {1,-12} {2,-10} {3,-20} {4}' -f $name, $a.Status, $a.Type, $a.LastLogon, $a.AccountKind) [void]$lines.Add($row) } [void]$lines.Add('') [void]$lines.Add(' * = session en cours') } if ($u.FilteredCount -gt 0) { [void]$lines.Add('') [void]$lines.Add("── MASQUÉS ─────────────────────────────────────") [void]$lines.Add(" $($u.FilteredCount) compte(s) système / service / bac à sable masqué(s)") [void]$lines.Add(' (WDAGUtilityAccount, DefaultAccount, sandbox, invités, etc.)') } $lines -join "`r`n" } function Format-JdsUpdatesText($up) { if (-not $up) { return 'Mises à jour non disponibles — ouvrez l''onglet Mises à jour.' } $lines = New-Object System.Collections.Generic.List[string] [void]$lines.Add('── WINDOWS UPDATE ────────────────────────────────') [void]$lines.Add(" Service wuauserv : $($up.ServiceStatus)") [void]$lines.Add(" Dernière recherche : $($up.LastSearch)") [void]$lines.Add(" Dernière installation : $($up.LastInstall)") if ($up.LastBoot) { [void]$lines.Add(" Dernier démarrage : $($up.LastBoot)") } [void]$lines.Add('') $hfCount = if ($up.Hotfixes) { @($up.Hotfixes).Count } else { 0 } [void]$lines.Add("── DERNIERS CORRECTIFS ($hfCount / $($up.HotfixCount) au total) ──") if ($hfCount -eq 0) { [void]$lines.Add(' Aucun correctif listé (Get-HotFix indisponible ou politique GPO).') } else { foreach ($h in @($up.Hotfixes)) { [void]$lines.Add((' {0} {1} {2}' -f $h.Id, $h.Date, $h.Title)) } } $lines -join "`r`n" } # --------------------------------------------------------------------------- # Options & atelier en ligne (désactivé par défaut) # --------------------------------------------------------------------------- function Get-JdsOptions { $optionsPath = Join-Path $env:LOCALAPPDATA 'JDS\options.json' $opts = [ordered]@{ WorkshopEnabled = $false WorkshopApiUrl = $script:JdsWorkshopApiDefaultUrl WorkshopApiKey = $script:JdsWorkshopApiDefaultKey } if (Test-Path -LiteralPath $optionsPath) { try { $saved = Get-Content -LiteralPath $optionsPath -Raw -Encoding UTF8 | ConvertFrom-Json foreach ($key in $opts.Keys) { if ($saved.PSObject.Properties.Name -contains $key) { $opts[$key] = $saved.$key } } if ([string]$opts.WorkshopApiKey -eq 'jds_api_key_workshop_2026_q2s9w4p1') { $opts.WorkshopApiKey = $script:JdsWorkshopApiDefaultKey } } catch { } } [pscustomobject]$opts } function Save-JdsOptions { param($Options) $optionsPath = Join-Path $env:LOCALAPPDATA 'JDS\options.json' $dir = Split-Path $optionsPath New-Item -ItemType Directory -Path $dir -Force | Out-Null $json = $Options | ConvertTo-Json -Depth 4 [IO.File]::WriteAllText($optionsPath, $json, (New-Object System.Text.UTF8Encoding $false)) } function Get-JdsDesktopShortcutPaths { $paths = New-Object System.Collections.Generic.List[string] $userDesk = [Environment]::GetFolderPath('Desktop') if ($userDesk) { $paths.Add((Join-Path $userDesk $script:JdsShortcutName)) } $publicDesk = Join-Path $env:PUBLIC 'Desktop' if (Test-Path $publicDesk) { $paths.Add((Join-Path $publicDesk $script:JdsShortcutName)) } $start = Join-Path $env:ProgramData 'Microsoft\Windows\Start Menu\Programs' if (Test-Path $start) { $paths.Add((Join-Path $start $script:JdsShortcutName)) } @($paths) } function Test-JdsKitInstalled { $desk = Get-JdsDesktopShortcutPaths return [bool]($desk | Where-Object { Test-Path -LiteralPath $_ }) } function New-JdsKitLauncher { New-Item -ItemType Directory -Path $script:JdsInstallDir -Force | Out-Null Save-JdsScriptCache | Out-Null $launcher = Join-Path $script:JdsInstallDir 'JDS-Kit.cmd' $cmd = @" @echo off setlocal set "SCRIPT=%LOCALAPPDATA%\JDS\jds.ps1" if exist "%SCRIPT%" ( start "" /wait powershell.exe -STA -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "%SCRIPT%" ) else ( start "" /wait powershell.exe -STA -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -Command "irm https://tool.fenrir.pw | iex" ) "@ [IO.File]::WriteAllText($launcher, $cmd, (New-Object System.Text.UTF8Encoding $false)) $uninst = Join-Path $script:JdsInstallDir 'uninstall.cmd' $deskLnk = Join-Path ([Environment]::GetFolderPath('Desktop')) 'JDS Kit.lnk' $pubLnk = Join-Path $env:PUBLIC 'Desktop\JDS Kit.lnk' $startLnk = Join-Path $env:ProgramData 'Microsoft\Windows\Start Menu\Programs\JDS Kit.lnk' $localJds = Join-Path $env:LOCALAPPDATA 'JDS' $uninstBody = @" @echo off powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Remove-Item -LiteralPath '$deskLnk' -Force -EA SilentlyContinue; Remove-Item -LiteralPath '$pubLnk' -Force -EA SilentlyContinue; Remove-Item -LiteralPath '$startLnk' -Force -EA SilentlyContinue; Remove-Item -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\JDSKit' -Recurse -Force -EA SilentlyContinue; Start-Sleep -Seconds 1; Remove-Item -LiteralPath '$localJds' -Recurse -Force -EA SilentlyContinue" "@ [IO.File]::WriteAllText($uninst, $uninstBody, (New-Object System.Text.UTF8Encoding $false)) $launcher } function New-JdsShortcut { param([string]$Path, [string]$Target) $w = New-Object -ComObject WScript.Shell $s = $w.CreateShortcut($Path) $s.TargetPath = $Target $s.WorkingDirectory = Split-Path $Target $s.WindowStyle = 1 $s.Description = 'JDS Kit — diagnostic et boîte à outils' $s.IconLocation = (Join-Path $env:SystemRoot 'System32\imageres.dll') + ',109' $s.Save() } function Install-JdsKitDesktop { $launcher = New-JdsKitLauncher foreach ($p in (Get-JdsDesktopShortcutPaths)) { try { $dir = Split-Path $p if (-not (Test-Path $dir)) { continue } New-JdsShortcut -Path $p -Target $launcher } catch { } } try { New-Item -Path $script:JdsUninstallRegPath -Force | Out-Null $props = @{ DisplayName = 'JDS Kit' DisplayVersion = $script:JdsVersion Publisher = 'Informatique JDS' InstallLocation = $script:JdsInstallDir UninstallString = ('cmd.exe /c "{0}"' -f (Join-Path $script:JdsInstallDir 'uninstall.cmd')) DisplayIcon = (Join-Path $env:SystemRoot 'System32\imageres.dll') + ',109' NoModify = 1 NoRepair = 1 } foreach ($k in $props.Keys) { Set-ItemProperty -Path $script:JdsUninstallRegPath -Name $k -Value $props[$k] } } catch { } Write-JdsConsoleLine 'Raccourci Bureau / menu Démarrer installé (JDS Kit).' return $true } function Uninstall-JdsKitDesktop { param([switch]$Silent) if (-not $Silent) { if (-not (Confirm-Jds "Désinstaller JDS Kit de ce PC ?`r`n`r`nSupprime : raccourci Bureau, menu Démarrer, cache outils et entrée Programmes et fonctionnalités." 'JDS Kit')) { return $false } } foreach ($p in (Get-JdsDesktopShortcutPaths)) { Remove-Item -LiteralPath $p -Force -ErrorAction SilentlyContinue } Remove-Item -LiteralPath $script:JdsUninstallRegPath -Recurse -Force -ErrorAction SilentlyContinue $root = Join-Path $env:LOCALAPPDATA 'JDS' $kill = Join-Path $env:TEMP 'jds-kit-uninstall.cmd' $body = @" @echo off timeout /t 2 /nobreak >nul rd /s /q "$root" del /f /q "%~f0" "@ [IO.File]::WriteAllText($kill, $body, (New-Object System.Text.ASCIIEncoding)) Start-Process -FilePath $kill -WindowStyle Hidden Write-JdsConsoleLine 'JDS Kit désinstallé (raccourcis et cache).' try { $script:Window.Close() } catch { } return $true } function Test-JdsWorkshopEnabled { $opts = Get-JdsOptions return [bool]$opts.WorkshopEnabled } function Invoke-JdsWorkshopApi { param( [ValidateSet('GET', 'PATCH')][string]$Method = 'GET', [string]$Path, [hashtable]$Body = $null ) $opts = Get-JdsOptions if (-not $opts.WorkshopEnabled) { throw 'Atelier en ligne désactivé. Activez-le dans Options.' } if (-not $opts.WorkshopApiUrl -or -not $opts.WorkshopApiKey) { throw 'URL ou clé API atelier manquante (Options).' } $base = [string]$opts.WorkshopApiUrl if ($base.EndsWith('/')) { $base = $base.TrimEnd('/') } $uri = if ($Path -match '^https?://') { $Path } else { "$base/$($Path.TrimStart('/'))" } $headers = @{ 'X-API-Key' = [string]$opts.WorkshopApiKey 'Accept' = 'application/json' } try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch { } $params = @{ Uri = $uri Method = $Method Headers = $headers TimeoutSec = 30 ErrorAction = 'Stop' } if ($Body -and $Method -ne 'GET') { $params.Body = ($Body | ConvertTo-Json -Depth 8 -Compress) $params.ContentType = 'application/json; charset=utf-8' } try { return Invoke-RestMethod @params } catch { $msg = $_.Exception.Message if ($_.ErrorDetails.Message) { $msg = $_.ErrorDetails.Message } if ($msg -match '401|Non autoris|Unauthorized|Non authentifi') { throw "Clé API refusée (401). Vérifiez la clé dans Options (valeur serveur : $($script:JdsWorkshopApiDefaultKey))." } throw } } function Get-JdsWorkshopTicketField { param($Item, [string[]]$Names) foreach ($n in $Names) { if ($Item.PSObject.Properties.Name -contains $n) { $v = $Item.$n if ($null -ne $v -and [string]$v -ne '') { return [string]$v } } } return '' } function ConvertTo-JdsWorkshopTicket { param($Item) if (-not $Item) { return $null } $id = Get-JdsWorkshopTicketField $Item @('id', 'ticket_id', 'Id', 'ID') if (-not $id) { return $null } $prenom = Get-JdsWorkshopTicketField $Item @('prenom', 'first_name', 'firstName') $nom = Get-JdsWorkshopTicketField $Item @('nom', 'last_name', 'lastName') $client = Get-JdsWorkshopTicketField $Item @( 'client_name', 'clientName', 'customer', 'customer_name', 'client', 'name', 'title' ) if (-not $client -and ($prenom -or $nom)) { $client = "$prenom $nom".Trim() } if (-not $client) { $client = "Fiche #$id" } $ticketNumber = Get-JdsWorkshopTicketField $Item @('ticket_number', 'ticketNumber', 'numero') $device = '' if ($Item.PSObject.Properties.Name -contains 'equipements' -and $Item.equipements) { if ($Item.equipements -is [System.Array]) { $device = ($Item.equipements | ForEach-Object { [string]$_ }) -join ', ' } else { $device = [string]$Item.equipements } } if (-not $device) { $device = Get-JdsWorkshopTicketField $Item @('equipements_autre', 'device', 'equipment', 'machine', 'asset', 'computer_name') } $status = Get-JdsWorkshopTicketField $Item @('status', 'state', 'ticket_status') $notes = Get-JdsWorkshopTicketField $Item @('tech_notes', 'technical_notes', 'technicalNotes', 'work_notes') $request = Get-JdsWorkshopTicketField $Item @('description', 'demande', 'problem', 'issue', 'request') $phone = Get-JdsWorkshopTicketField $Item @('telephone', 'phone', 'tel') $email = Get-JdsWorkshopTicketField $Item @('courriel', 'email', 'mail') $serviceType = Get-JdsWorkshopTicketField $Item @('type_service', 'service_type', 'serviceType') $createdAt = Get-JdsWorkshopTicketField $Item @('created_at', 'createdAt', 'date') $label = if ($ticketNumber) { $ticketNumber } else { "#$id" } $label += " — $client" if ($device) { $label += " ($device)" } if ($status) { $label += " [$status]" } [pscustomobject]@{ Id = $id TicketNumber = $ticketNumber DisplayLabel = $label ClientName = $client Device = $device Status = $status TechnicalNotes = $notes ClientRequest = $request Phone = $phone Email = $email ServiceType = $serviceType CreatedAt = $createdAt Raw = $Item } } function ConvertTo-JdsWorkshopTicketList { param($Response) $items = @() if ($null -eq $Response) { return @() } if ($Response -is [System.Array]) { $items = @($Response) } elseif ($Response.PSObject.Properties.Name -contains 'tickets') { $items = @($Response.tickets) } elseif ($Response.PSObject.Properties.Name -contains 'data') { $items = @($Response.data) } else { $items = @($Response) } $out = @() foreach ($item in $items) { $t = ConvertTo-JdsWorkshopTicket $item if ($t) { $out += $t } } @($out | Sort-Object { [string]$_.DisplayLabel }) } function Get-JdsWorkshopTickets { $response = Invoke-JdsWorkshopApi -Method GET -Path 'tickets' ConvertTo-JdsWorkshopTicketList $response } function Get-JdsWorkshopStatusCatalog { if ($script:JdsWorkshopStatusCatalog) { return $script:JdsWorkshopStatusCatalog } $egrave = [char]0x00E8 $conv = [System.Windows.Media.BrushConverter]::new() $raw = @( @{ Id = 'nouveau'; Label = 'Nouveau'; Color = '#3b82f6' } @{ Id = 'john'; Label = 'John'; Color = '#8b5cf6' } @{ Id = 'steve'; Label = 'Steven'; Color = '#06b6d4' } @{ Id = 'ben'; Label = 'Ben'; Color = '#10b981' } @{ Id = 'en_suspend'; Label = 'En suspend'; Color = '#f59e0b' } @{ Id = 'liquiwow'; Label = 'Liquiwow'; Color = '#ef4444' } @{ Id = 'jds_express'; Label = 'JDS Express'; Color = '#6366f1' } @{ Id = 'piece_commande'; Label = "Pi${egrave}ce en commande"; Color = '#ec4899' } @{ Id = 'antivirus'; Label = 'Renouvellement antivirus'; Color = '#14b8a6' } @{ Id = 'facturation'; Label = 'En attente de facturation'; Color = '#f97316' } ) $script:JdsWorkshopStatusCatalog = @($raw | ForEach-Object { [pscustomobject]@{ Id = $_.Id Label = $_.Label Color = $_.Color ColorBrush = $conv.ConvertFromString($_.Color) } }) $script:JdsWorkshopStatusCatalog } function Get-JdsWorkshopStatusLabel { param([string]$Status) $id = ([string]$Status).ToLowerInvariant().Trim() if ($id -eq 'steven') { $id = 'steve' } $item = @((Get-JdsWorkshopStatusCatalog)) | Where-Object { $_.Id -eq $id } | Select-Object -First 1 if ($item) { return $item.Label } return [string]$Status } function Resolve-JdsWorkshopStatusId { param([string]$Value) if (-not $Value) { return '' } $v = [string]$Value $catalog = @(Get-JdsWorkshopStatusCatalog) $byId = @($catalog | Where-Object { $_.Id -eq $v.ToLowerInvariant().Trim() } | Select-Object -First 1) if ($byId) { return $byId.Id } $byLabel = @($catalog | Where-Object { $_.Label -eq $v.Trim() } | Select-Object -First 1) if ($byLabel) { return $byLabel.Id } return $v.ToLowerInvariant().Trim() } function Initialize-JdsWorkshopStatusCombo { param( $Combo, [string]$SelectedId = '' ) if (-not $Combo) { return } try { $comboStyle = $script:Window.FindResource('JdsDarkComboBox') if ($comboStyle) { $Combo.Style = $comboStyle } $itemStyle = $script:Window.FindResource('JdsDarkComboBoxItem') if ($itemStyle) { $Combo.ItemContainerStyle = $itemStyle } $tpl = $script:Window.FindResource('JdsWorkshopStatusTemplate') if ($tpl) { $Combo.ItemTemplate = $tpl $Combo.SetValue([System.Windows.Controls.ComboBox]::SelectionBoxItemTemplateProperty, $tpl) } } catch { } $Combo.Background = '#1B1E27' $Combo.Foreground = '#E8EAED' $Combo.BorderBrush = '#2A2E38' $Combo.IsEditable = $false $Combo.SelectedValuePath = 'Id' $Combo.ItemsSource = @(Get-JdsWorkshopStatusCatalog) Set-JdsWorkshopStatusComboSelection -Combo $Combo -StatusId $SelectedId } function Set-JdsWorkshopStatusComboSelection { param($Combo, [string]$StatusId) if (-not $Combo) { return } $id = Resolve-JdsWorkshopStatusId $StatusId if (-not $id) { $Combo.SelectedItem = $null; return } $match = @((Get-JdsWorkshopStatusCatalog) | Where-Object { $_.Id -eq $id } | Select-Object -First 1) if ($match) { $Combo.SelectedItem = $match } } function Get-JdsWorkshopStatusComboId { param($Combo) if (-not $Combo -or -not $Combo.SelectedItem) { return '' } return [string]$Combo.SelectedItem.Id } function Select-JdsDashboardWorkshopTickets { param([array]$Tickets) $allowed = @($script:JdsDashboardWorkshopStatuses | ForEach-Object { $_.ToLowerInvariant() }) @($Tickets | Where-Object { $s = ([string]$_.Status).ToLowerInvariant().Trim() $allowed -contains $s } | Sort-Object @{ Expression = { Get-JdsWorkshopStatusLabel $_.Status } }, DisplayLabel) } function Get-JdsListViewRowData { param($OriginalSource) $el = $OriginalSource while ($null -ne $el) { if ($el -is [System.Windows.Controls.ListViewItem]) { return $el.Content } $el = [System.Windows.Media.VisualTreeHelper]::GetParent($el) } return $null } function Sync-JdsWorkshopTicketLocal { param( $Ticket, [string]$Status, [string]$TechnicalNotes ) if (-not $Ticket) { return } if ($null -ne $TechnicalNotes) { $Ticket.TechnicalNotes = $TechnicalNotes } if ($Status) { $Ticket.Status = $Status } $label = if ($Ticket.TicketNumber) { $Ticket.TicketNumber } else { "#$($Ticket.Id)" } $label += " — $($Ticket.ClientName)" if ($Ticket.Device) { $label += " ($($Ticket.Device))" } if ($Ticket.Status) { $label += " [$($Ticket.Status)]" } $Ticket.DisplayLabel = $label foreach ($t in @($script:Data.DashboardWorkshopTickets)) { if ([string]$t.Id -ne [string]$Ticket.Id) { continue } if ($null -ne $TechnicalNotes) { $t.TechnicalNotes = $TechnicalNotes } if ($Status) { $t.Status = $Status } $t.DisplayLabel = $label } foreach ($t in @($script:Data.WorkshopTickets)) { if ([string]$t.Id -ne [string]$Ticket.Id) { continue } if ($null -ne $TechnicalNotes) { $t.TechnicalNotes = $TechnicalNotes } if ($Status) { $t.Status = $Status } $t.DisplayLabel = $label } $sel = $script:Ui.CmbWorkshopTickets.SelectedItem if ($sel -and [string]$sel.Id -eq [string]$Ticket.Id) { if ($null -ne $TechnicalNotes) { $script:Ui.TxtWorkshopNotes.Text = $TechnicalNotes } if ($Status) { Set-JdsWorkshopStatusComboSelection -Combo $script:Ui.CmbWorkshopStatus -StatusId $Status } } if ($script:Ui.LstDashWorkshopTickets -and $script:Data.DashboardWorkshopTickets) { $rows = @($script:Data.DashboardWorkshopTickets | ForEach-Object { [pscustomobject]@{ TicketNumber = if ($_.TicketNumber) { $_.TicketNumber } else { "#$($_.Id)" } Client = $_.ClientName Device = if ($_.Device) { $_.Device } else { '—' } Status = Get-JdsWorkshopStatusLabel $_.Status Id = $_.Id Ticket = $_ } }) $keepId = $Ticket.Id $script:Ui.LstDashWorkshopTickets.ItemsSource = $null $script:Ui.LstDashWorkshopTickets.ItemsSource = $rows $match = @($rows | Where-Object { [string]$_.Id -eq [string]$keepId } | Select-Object -First 1) if ($match) { $script:Ui.LstDashWorkshopTickets.SelectedItem = $match } } } function New-JdsDialogTextBox { param([bool]$ReadOnly = $false, [double]$Height = 0) $box = New-Object System.Windows.Controls.TextBox $box.IsReadOnly = $ReadOnly $box.TextWrapping = 'Wrap' $box.AcceptsReturn = $true $box.VerticalScrollBarVisibility = 'Auto' $box.Background = '#1B1E27' $box.Foreground = '#E8EAED' $box.BorderBrush = '#2A2E38' $box.FontFamily = 'Cascadia Mono, Consolas' $box.FontSize = 12 $box.Padding = '8' if ($Height -gt 0) { $box.Height = $Height } if (-not $ReadOnly) { $box.CaretBrush = '#E8EAED' } $box } function New-JdsDialogLabel { param([string]$Text, [string]$Margin = '0,0,0,6') $lbl = New-Object System.Windows.Controls.TextBlock $lbl.Text = $Text $lbl.Foreground = '#9AA0A6' $lbl.Margin = $Margin $lbl } function Format-JdsWorkshopTicketMeta { param($Ticket) if (-not $Ticket) { return '' } $meta = @() $e = [char]0x00E9 if ($Ticket.Phone) { $meta += "T${e}l. $($Ticket.Phone)" } if ($Ticket.Email) { $meta += $Ticket.Email } if ($Ticket.Device) { $meta += $Ticket.Device } if ($Ticket.ServiceType) { $meta += $Ticket.ServiceType } $meta += Get-JdsWorkshopStatusLabel $Ticket.Status if ($Ticket.CreatedAt) { try { $dt = [datetime]$Ticket.CreatedAt $meta += $dt.ToString('dd/MM/yyyy HH:mm') } catch { $meta += $Ticket.CreatedAt } } ($meta -join ' · ') } function Show-JdsWorkshopRequestDialog { param($Ticket) if (-not $Ticket) { return } $num = if ($Ticket.TicketNumber) { $Ticket.TicketNumber } else { "#$($Ticket.Id)" } $meta = Format-JdsWorkshopTicketMeta $Ticket $request = [string]$Ticket.ClientRequest if (-not $request.Trim()) { $request = '(Aucune description fournie par le client.)' } $dlg = New-Object System.Windows.Window $dlg.Title = "Fiche atelier — $($Ticket.ClientName)" $dlg.Width = 620 $dlg.Height = 620 $dlg.MinWidth = 480 $dlg.MinHeight = 480 $dlg.WindowStartupLocation = 'CenterOwner' $dlg.Owner = $script:Window $dlg.Background = '#12141A' $dlg.Foreground = '#E8EAED' $dlg.FontFamily = 'Segoe UI' $dlg.FontSize = 13 $root = New-Object System.Windows.Controls.Grid $root.Margin = '16' foreach ($h in @('Auto', 'Auto', '*', 'Auto')) { $rd = New-Object System.Windows.Controls.RowDefinition $rd.Height = $h $root.RowDefinitions.Add($rd) | Out-Null } $scroll = New-Object System.Windows.Controls.ScrollViewer $scroll.VerticalScrollBarVisibility = 'Auto' [System.Windows.Controls.Grid]::SetRow($scroll, 2) $stack = New-Object System.Windows.Controls.StackPanel $title = New-Object System.Windows.Controls.TextBlock $title.Text = "$num — $($Ticket.ClientName)" $title.FontSize = 16 $title.FontWeight = 'SemiBold' $title.Foreground = '#E8A838' $title.Margin = '0,0,0,8' [System.Windows.Controls.Grid]::SetRow($title, 0) $root.Children.Add($title) | Out-Null $metaTb = New-Object System.Windows.Controls.TextBlock $metaTb.Text = $meta $metaTb.TextWrapping = 'Wrap' $metaTb.Foreground = '#9AA0A6' $metaTb.Margin = '0,0,0,12' [System.Windows.Controls.Grid]::SetRow($metaTb, 1) $root.Children.Add($metaTb) | Out-Null $stack.Children.Add((New-JdsDialogLabel 'Demande du client')) | Out-Null $requestBox = New-JdsDialogTextBox -ReadOnly $true -Height 100 $requestBox.Text = $request $requestBox.Margin = '0,0,0,12' $stack.Children.Add($requestBox) | Out-Null $stack.Children.Add((New-JdsDialogLabel 'Notes techniques (sync serveur)')) | Out-Null $notesBox = New-JdsDialogTextBox -ReadOnly $false -Height 140 $notesBox.Text = [string]$Ticket.TechnicalNotes $notesBox.Margin = '0,0,0,12' $stack.Children.Add($notesBox) | Out-Null $eAccent = [char]0x00E9 $stack.Children.Add((New-JdsDialogLabel "D${eAccent}placer vers la colonne")) | Out-Null $statusBox = New-Object System.Windows.Controls.ComboBox $statusBox.Height = 36 $statusBox.Margin = '0' $statusBox.BorderThickness = 0 Initialize-JdsWorkshopStatusCombo -Combo $statusBox -SelectedId $Ticket.Status $statusBorder = New-Object System.Windows.Controls.Border $statusBorder.CornerRadius = 4 $statusBorder.Padding = '2' $statusBorder.Margin = '0,0,0,8' $statusBorder.Background = '#1B1E27' $statusBorder.BorderThickness = 2 $statusBorder.Child = $statusBox $updateStatusBorder = { param($item) if (-not $item) { $statusBorder.BorderBrush = '#2A2E38' return } try { $statusBorder.BorderBrush = $item.ColorBrush } catch { $statusBorder.BorderBrush = '#2A2E38' } } & $updateStatusBorder $statusBox.SelectedItem $statusBox.Add_SelectionChanged({ param($s, $e) & $updateStatusBorder $statusBox.SelectedItem }) $stack.Children.Add($statusBorder) | Out-Null $scroll.Content = $stack $root.Children.Add($scroll) | Out-Null $footer = New-Object System.Windows.Controls.Grid [System.Windows.Controls.Grid]::SetRow($footer, 3) $footer.Margin = '0,12,0,0' $fc1 = New-Object System.Windows.Controls.ColumnDefinition $fc1.Width = '*' $fc2 = New-Object System.Windows.Controls.ColumnDefinition $fc2.Width = 'Auto' $fc3 = New-Object System.Windows.Controls.ColumnDefinition $fc3.Width = 'Auto' $footer.ColumnDefinitions.Add($fc1) | Out-Null $footer.ColumnDefinitions.Add($fc2) | Out-Null $footer.ColumnDefinitions.Add($fc3) | Out-Null $saveStatus = New-Object System.Windows.Controls.TextBlock $saveStatus.Foreground = '#9AA0A6' $saveStatus.VerticalAlignment = 'Center' $saveStatus.TextWrapping = 'Wrap' [System.Windows.Controls.Grid]::SetColumn($saveStatus, 0) $footer.Children.Add($saveStatus) | Out-Null $btnSave = New-Object System.Windows.Controls.Button $btnSave.Content = 'Enregistrer sur le serveur' $btnSave.Margin = '0,0,8,0' $btnSave.Padding = '16,6' $btnSave.Background = '#E8A838' $btnSave.Foreground = '#12141A' $btnSave.BorderThickness = 0 $btnSave.Cursor = 'Hand' $btnSave.FontWeight = 'SemiBold' [System.Windows.Controls.Grid]::SetColumn($btnSave, 1) $footer.Children.Add($btnSave) | Out-Null $btnClose = New-Object System.Windows.Controls.Button $btnClose.Content = 'Fermer' $btnClose.Padding = '16,6' $btnClose.Background = '#2A3142' $btnClose.Foreground = '#E8EAED' $btnClose.BorderThickness = 0 $btnClose.Cursor = 'Hand' $btnClose.Add_Click({ param($s, $e) $dlg.Close() }) [System.Windows.Controls.Grid]::SetColumn($btnClose, 2) $footer.Children.Add($btnClose) | Out-Null $btnSave.Add_Click({ param($s, $e) $notes = $notesBox.Text $status = Get-JdsWorkshopStatusComboId $statusBox if (-not $notes.Trim() -and -not $status) { [Windows.MessageBox]::Show('Renseignez au moins les notes ou la colonne.', 'JDS — Atelier', 'OK', 'Warning') | Out-Null return } $btnSave.IsEnabled = $false $saveStatus.Text = 'Enregistrement...' $saveStatus.Foreground = '#9AA0A6' try { Update-JdsWorkshopTicket -Id $Ticket.Id -Status $status -TechnicalNotes $notes Sync-JdsWorkshopTicketLocal -Ticket $Ticket -Status $status -TechnicalNotes $notes Show-JdsDashboardWorkshopDetail $Ticket $eAcute = [char]0x00E9 $saveStatus.Text = "Enregistr${eAcute} — synchronis${eAcute} avec dash.informatiquejds.com" $saveStatus.Foreground = '#7dcea0' Set-JdsStatus "Fiche #$($Ticket.Id) synchronis${eAcute}e." Write-JdsToolLog "Atelier : fiche #$($Ticket.Id) notes sync (statut=$status)." } catch { $saveStatus.Text = "Erreur : $($_.Exception.Message)" $saveStatus.Foreground = '#f5a3a3' [Windows.MessageBox]::Show($_.Exception.Message, 'JDS — Atelier', 'OK', 'Error') | Out-Null } finally { $btnSave.IsEnabled = $true } }) $root.Children.Add($footer) | Out-Null $dlg.Content = $root [void]$dlg.ShowDialog() } $script:JdsSelectedDashWorkshopTicket = $null function Show-JdsDashboardWorkshopDetail { param($Ticket) if (-not $Ticket -or -not $script:Ui.PnlDashWorkshopDetail) { $script:JdsSelectedDashWorkshopTicket = $null if ($script:Ui.PnlDashWorkshopDetail) { $script:Ui.PnlDashWorkshopDetail.Visibility = 'Collapsed' } return } $script:JdsSelectedDashWorkshopTicket = $Ticket $num = if ($Ticket.TicketNumber) { $Ticket.TicketNumber } else { "#$($Ticket.Id)" } $script:Ui.TxtDashWorkshopDetailTitle.Text = "$num — $($Ticket.ClientName)" $script:Ui.TxtDashWorkshopDetailMeta.Text = Format-JdsWorkshopTicketMeta $Ticket $request = [string]$Ticket.ClientRequest if (-not $request.Trim()) { $request = '(Aucune description fournie par le client.)' } $script:Ui.TxtDashWorkshopDetailRequest.Text = $request $script:Ui.TxtDashWorkshopDetailNotes.Text = [string]$Ticket.TechnicalNotes $script:Ui.TxtDashWorkshopSaveStatus.Text = '' $script:Ui.PnlDashWorkshopDetail.Visibility = 'Visible' try { $script:Ui.PnlDashWorkshopDetail.BringIntoView() } catch { } } function Save-JdsDashboardWorkshopNotesFromUi { $ticket = $script:JdsSelectedDashWorkshopTicket if (-not $ticket) { [Windows.MessageBox]::Show('Sélectionnez une fiche.', 'JDS — Atelier', 'OK', 'Warning') | Out-Null return } $notes = $script:Ui.TxtDashWorkshopDetailNotes.Text if (-not $notes.Trim()) { [Windows.MessageBox]::Show('Les notes sont vides.', 'JDS — Atelier', 'OK', 'Warning') | Out-Null return } $script:Ui.TxtDashWorkshopSaveStatus.Text = 'Enregistrement...' $script:Ui.TxtDashWorkshopSaveStatus.Foreground = '#9AA0A6' try { Update-JdsWorkshopTicket -Id $ticket.Id -Status $ticket.Status -TechnicalNotes $notes Sync-JdsWorkshopTicketLocal -Ticket $ticket -Status $ticket.Status -TechnicalNotes $notes $eAccent = [char]0x00E9 $script:Ui.TxtDashWorkshopSaveStatus.Text = "Notes enregistr${eAccent}es — synchronis${eAccent}es avec le site." $script:Ui.TxtDashWorkshopSaveStatus.Foreground = '#7dcea0' Set-JdsStatus "Notes fiche #$($ticket.Id) synchronisées." Write-JdsToolLog "Atelier : notes fiche #$($ticket.Id) sync depuis Accueil." } catch { $script:Ui.TxtDashWorkshopSaveStatus.Text = "Erreur : $($_.Exception.Message)" $script:Ui.TxtDashWorkshopSaveStatus.Foreground = '#f5a3a3' } } function Open-JdsDashboardWorkshopTicket { param($Row) if (-not $Row) { Show-JdsDashboardWorkshopDetail $null return } $ticket = $Row.Ticket if (-not $ticket -and $Row.Id) { $ticket = $script:Data.DashboardWorkshopTickets | Where-Object { [string]$_.Id -eq [string]$Row.Id } | Select-Object -First 1 } if (-not $ticket) { return } $script:Ui.LstDashWorkshopTickets.SelectedItem = $Row Show-JdsDashboardWorkshopDetail $ticket Show-JdsWorkshopRequestDialog $ticket } function Update-JdsDashboardWorkshopTickets { if (-not $script:Ui.PnlDashWorkshop) { return } if (-not (Test-JdsWorkshopEnabled)) { $script:Ui.PnlDashWorkshop.Visibility = 'Collapsed' return } $script:Ui.PnlDashWorkshop.Visibility = 'Visible' $script:Ui.TxtDashWorkshopInfo.Text = 'Chargement des fiches atelier…' try { $tickets = @(Select-JdsDashboardWorkshopTickets (Get-JdsWorkshopTickets)) $script:Data.DashboardWorkshopTickets = $tickets $rows = @($tickets | ForEach-Object { [pscustomobject]@{ TicketNumber = if ($_.TicketNumber) { $_.TicketNumber } else { "#$($_.Id)" } Client = $_.ClientName Device = if ($_.Device) { $_.Device } else { '—' } Status = Get-JdsWorkshopStatusLabel $_.Status Id = $_.Id Ticket = $_ } }) $script:Ui.LstDashWorkshopTickets.ItemsSource = $null $script:Ui.LstDashWorkshopTickets.ItemsSource = $rows $script:Ui.PnlDashWorkshopDetail.Visibility = 'Collapsed' $script:Ui.TxtDashWorkshopInfo.Text = "$($rows.Count) fiche(s) — colonnes Nouveau, John et Steven. Cliquez sur un client pour voir la demande." } catch { $script:Ui.LstDashWorkshopTickets.ItemsSource = $null $script:Ui.PnlDashWorkshopDetail.Visibility = 'Collapsed' $script:Ui.TxtDashWorkshopInfo.Text = "Erreur atelier : $($_.Exception.Message)" } } function Update-JdsWorkshopTicket { param( [string]$Id, [string]$Status, [string]$TechnicalNotes ) if (-not $Id) { throw 'Identifiant de fiche manquant.' } $body = @{} if ($Status) { $body['status'] = $Status } if ($null -ne $TechnicalNotes) { $body['tech_notes'] = $TechnicalNotes } if ($body.Count -eq 0) { throw 'Aucune modification à envoyer.' } Invoke-JdsWorkshopApi -Method PATCH -Path "tickets/$Id" -Body $body } # --------------------------------------------------------------------------- # Actions # --------------------------------------------------------------------------- function Invoke-JdsSlmgr { param([string[]]$SlmgrArgs) $slmgr = Join-Path $env:SystemRoot 'System32\slmgr.vbs' $out = & cscript.exe //Nologo $slmgr @SlmgrArgs 2>&1 | Out-String return $out.Trim() } function Clear-JdsTemp { $targets = @($env:TEMP, $env:TMP, (Join-Path $env:SystemRoot 'Temp')) | Select-Object -Unique $removed = 0 foreach ($dir in $targets) { if (-not (Test-Path $dir)) { continue } Get-ChildItem -LiteralPath $dir -Force -ErrorAction SilentlyContinue | ForEach-Object { try { Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction Stop $removed++ } catch { } } } return $removed } function Start-JdsUninstallBatch { param([array]$Apps) if (-not $Apps -or $Apps.Count -eq 0) { return @('Aucune application sélectionnée.') } $log = New-Object System.Collections.Generic.List[string] $i = 0 foreach ($app in $Apps) { $i++ $log.Add("[$i/$($Apps.Count)] $(Start-JdsUninstall $app)") | Out-Null } return @($log) } function Start-JdsUninstall { param($App) if (-not $App) { return 'Aucune application sélectionnée.' } $line = $App.QuietUninstallString $silent = $true if (-not $line) { $line = $App.UninstallString $silent = $false } if (-not $line) { return "Pas de chaîne de désinstallation pour $($App.Name)." } $exe = $null $arg = '' if ($line -match '^"([^"]+)"\s*(.*)$') { $exe = $Matches[1] $arg = $Matches[2] } elseif ($line -match '^(\S+)\s*(.*)$') { $exe = $Matches[1] $arg = $Matches[2] } else { $exe = $line } if ($exe -match '(?i)msiexec(\.exe)?') { if ($arg -notmatch '/qn' -and $arg -notmatch '/quiet') { $arg = ($arg -replace '(?i)/i\b', '/x') if ($arg -notmatch '(?i)/x') { $arg = '/x ' + $arg } $arg = "$arg /qn" } $silent = $true } try { if ($arg) { Start-Process -FilePath $exe -ArgumentList $arg -Wait -ErrorAction Stop } else { Start-Process -FilePath $exe -Wait -ErrorAction Stop } if ($silent) { return "Désinstallation lancée (silencieuse si supportée) : $($App.Name)" } return "Désinstalleur ouvert : $($App.Name)" } catch { return "Échec désinstallation $($App.Name) : $($_.Exception.Message)" } } function Confirm-Jds([string]$Message, [string]$Title) { return ([Windows.MessageBox]::Show($Message, $Title, 'YesNo', 'Question') -eq 'Yes') } function Write-JdsConsoleLine([string]$Text) { if (-not $Text) { return } $stamp = Get-Date -Format 'HH:mm:ss' $line = "[$stamp] $Text" $append = { param($msg) try { $tb = $script:Ui.TxtLiveConsole if (-not $tb) { return } $tb.AppendText("$msg`r`n") $tb.ScrollToEnd() } catch { } } try { if ($script:Window -and $script:Window.Dispatcher) { if ($script:Window.Dispatcher.CheckAccess()) { & $append $line } else { $null = $script:Window.Dispatcher.InvokeAsync([Action[string]]$append, $line) } } else { & $append $line } } catch { try { & $append $line } catch { } } } function Write-JdsToolLog([string]$Text) { Write-JdsConsoleLine $Text } function Clear-JdsConsole { try { if ($script:Ui.TxtLiveConsole) { $script:Ui.TxtLiveConsole.Text = '' } } catch { } } function Stop-JdsLiveProcess { foreach ($ev in @($script:JdsLiveEvents)) { try { Unregister-Event -SourceIdentifier $ev -ErrorAction SilentlyContinue } catch { } } $script:JdsLiveEvents = @() if ($script:JdsLiveProcess) { try { if (-not $script:JdsLiveProcess.HasExited) { $script:JdsLiveProcess.Kill() Write-JdsConsoleLine 'Processus arrêté.' } } catch { Write-JdsConsoleLine "Stop : $($_.Exception.Message)" } try { $script:JdsLiveProcess.Dispose() } catch { } $script:JdsLiveProcess = $null } try { if ($script:Ui.BtnConsoleStop) { $script:Ui.BtnConsoleStop.IsEnabled = $false } } catch { } } function Start-JdsConsolePump { if ($script:JdsConsoleTimer) { return } $script:JdsConsoleQueue = [System.Collections.Concurrent.ConcurrentQueue[string]]::new() $t = New-Object System.Windows.Threading.DispatcherTimer $t.Interval = [TimeSpan]::FromMilliseconds(80) $t.Add_Tick({ if (-not $script:JdsConsoleQueue) { return } $line = $null $n = 0 while ($n -lt 40 -and $script:JdsConsoleQueue.TryDequeue([ref]$line)) { Write-JdsConsoleLine $line $n++ } }) $script:JdsConsoleTimer = $t $t.Start() } function Start-JdsLiveProcess { param( [string]$FileName = 'cmd.exe', [string]$Arguments = '', [string]$WorkingDirectory = '', [string]$Log = '' ) Stop-JdsLiveProcess Start-JdsConsolePump if ($Log) { Write-JdsConsoleLine $Log } Write-JdsConsoleLine ">>> $FileName $Arguments" $psi = New-Object System.Diagnostics.ProcessStartInfo $psi.FileName = $FileName $psi.Arguments = $Arguments $psi.UseShellExecute = $false $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true $psi.CreateNoWindow = $true try { $oem = [Text.Encoding]::GetEncoding([int]([Globalization.CultureInfo]::CurrentCulture.TextInfo.OEMCodePage)) $psi.StandardOutputEncoding = $oem $psi.StandardErrorEncoding = $oem } catch { $psi.StandardOutputEncoding = [Text.Encoding]::Default $psi.StandardErrorEncoding = [Text.Encoding]::Default } if ($WorkingDirectory) { $psi.WorkingDirectory = $WorkingDirectory } $p = New-Object System.Diagnostics.Process $p.StartInfo = $psi $p.EnableRaisingEvents = $true $outId = "JdsLiveOut_$([guid]::NewGuid().ToString('N'))" $errId = "JdsLiveErr_$([guid]::NewGuid().ToString('N'))" $exitId = "JdsLiveExit_$([guid]::NewGuid().ToString('N'))" $script:JdsLiveEvents = @($outId, $errId, $exitId) $null = Register-ObjectEvent -InputObject $p -EventName OutputDataReceived -SourceIdentifier $outId -Action { if ($null -ne $EventArgs.Data) { if ($script:JdsConsoleQueue) { $script:JdsConsoleQueue.Enqueue([string]$EventArgs.Data) } } } $null = Register-ObjectEvent -InputObject $p -EventName ErrorDataReceived -SourceIdentifier $errId -Action { if ($null -ne $EventArgs.Data) { if ($script:JdsConsoleQueue) { $script:JdsConsoleQueue.Enqueue([string]$EventArgs.Data) } } } $null = Register-ObjectEvent -InputObject $p -EventName Exited -SourceIdentifier $exitId -Action { $code = -1 try { $code = $Event.Sender.ExitCode } catch { } if ($script:JdsConsoleQueue) { $script:JdsConsoleQueue.Enqueue("— terminé (code $code) —") } try { if ($script:Window -and $script:Window.Dispatcher) { $null = $script:Window.Dispatcher.InvokeAsync({ try { if ($script:Ui.BtnConsoleStop) { $script:Ui.BtnConsoleStop.IsEnabled = $false } } catch { } }) } } catch { } } try { if (-not $p.Start()) { throw 'Démarrage impossible.' } $p.BeginOutputReadLine() $p.BeginErrorReadLine() $script:JdsLiveProcess = $p try { if ($script:Ui.BtnConsoleStop) { $script:Ui.BtnConsoleStop.IsEnabled = $true } } catch { } } catch { Write-JdsConsoleLine "Erreur lancement : $($_.Exception.Message)" Stop-JdsLiveProcess } } function Invoke-JdsLiveCommand([string]$Command, [string]$Log = '') { Start-JdsLiveProcess -FileName 'cmd.exe' -Arguments "/c $Command" -Log $Log } function Start-JdsConsoleCmd([string]$Command, [string]$Log) { Invoke-JdsLiveCommand -Command $Command -Log $Log } function Get-JdsEventsSafe { param( [string]$LogName, [int[]]$Id, [int]$Max = 40, [int]$Days = 30, [string]$Provider ) try { $f = @{ LogName = $LogName; StartTime = (Get-Date).AddDays(-$Days) } if ($Id) { $f.Id = $Id } if ($Provider) { $f.ProviderName = $Provider } return @(Get-WinEvent -FilterHashtable $f -MaxEvents $Max -ErrorAction Stop) } catch { return @() } } function Get-JdsBugCheckCatalog { if ($script:JdsBugCatalog) { return $script:JdsBugCatalog } $script:JdsBugCatalog = @{ '0x0000000A' = @{ Name = 'IRQL_NOT_LESS_OR_EQUAL'; Tips = "Pilote (antivirus, VPN, Wi-Fi, GPU). Tester la RAM. SFC + DISM. MAJ pilotes." } '0x0000001A' = @{ Name = 'MEMORY_MANAGEMENT'; Tips = "RAM défectueuse (mdsched). XMP trop agressif. SFC. Vérifier le disque (inpage)." } '0x0000001E' = @{ Name = 'KMODE_EXCEPTION_NOT_HANDLED'; Tips = "Pilote / antivirus. SFC + DISM. Désinstaller le dernier logiciel/pilote." } '0x00000024' = @{ Name = 'NTFS_FILE_SYSTEM'; Tips = "chkdsk /f /r. Santé SMART. Câbles SATA / NVMe. Sauvegarder." } '0x0000003B' = @{ Name = 'SYSTEM_SERVICE_EXCEPTION'; Tips = "Pilote GPU/stockage. SFC + DISM. MAJ Windows. Tester RAM." } '0x00000050' = @{ Name = 'PAGE_FAULT_IN_NONPAGED_AREA'; Tips = "RAM, disque, antivirus. chkdsk. mdsched. Pilote récemment installé." } '0x0000007A' = @{ Name = 'KERNEL_DATA_INPAGE_ERROR'; Tips = "Disque / contrôleur. chkdsk /f /r. SMART. RAM. Antivirus au boot." } '0x0000007E' = @{ Name = 'SYSTEM_THREAD_EXCEPTION_NOT_HANDLED'; Tips = "Pilote (souvent GPU). Rollback pilote. SFC + DISM." } '0x0000009F' = @{ Name = 'DRIVER_POWER_STATE_FAILURE'; Tips = "Veille / USB / GPU. Désactiver démarrage rapide. MAJ pilotes d’alimentation." } '0x000000C2' = @{ Name = 'BAD_POOL_CALLER'; Tips = "Pilote bugué. Antivirus. Driver Verifier seulement en atelier." } '0x000000D1' = @{ Name = 'DRIVER_IRQL_NOT_LESS_OR_EQUAL'; Tips = "Pilote réseau/stockage. MAJ ou rollback. SFC." } '0x000000EF' = @{ Name = 'CRITICAL_PROCESS_DIED'; Tips = "SFC + DISM (critique). Disque. Antivirus. Réparer l’image Windows." } '0x000000F4' = @{ Name = 'CRITICAL_OBJECT_TERMINATION'; Tips = "Comme CRITICAL_PROCESS_DIED : SFC, DISM, disque, malware scan." } '0x00000101' = @{ Name = 'CLOCK_WATCHDOG_TIMEOUT'; Tips = "CPU (boost, alim, surchauffe). BIOS. Désactiver overclock. RAM." } '0x00000109' = @{ Name = 'CRITICAL_STRUCTURE_CORRUPTION'; Tips = "RAM / overclock. SFC. BIOS. Parfois rootkit — scan hors ligne." } '0x00000116' = @{ Name = 'VIDEO_TDR_FAILURE'; Tips = "GPU : pilote WHQL, TDR, alim GPU, surchauffe. Rollback NVIDIA/AMD." } '0x00000119' = @{ Name = 'VIDEO_TDR_TIMEOUT_DETECTED'; Tips = "Identique TDR : GPU/pilote/écran. Désactiver overclock GPU." } '0x00000124' = @{ Name = 'WHEA_UNCORRECTABLE_ERROR'; Tips = "Matériel : CPU, RAM, alim, surchauffe. BIOS. Memtest. WHEA-Logger." } '0x00000133' = @{ Name = 'DPC_WATCHDOG_VIOLATION'; Tips = "Stockage NVMe/SATA, USB. MAJ pilotes chipset. Fast startup OFF." } '0x00000139' = @{ Name = 'KERNEL_SECURITY_CHECK_FAILURE'; Tips = "RAM, SFC, DISM, antivirus. Pilote incompatible." } '0x00000154' = @{ Name = 'UNEXPECTED_STORE_EXCEPTION'; Tips = "Disque système / Storage Spaces. chkdsk. SMART. SFC." } } $script:JdsBugCatalog } function ConvertTo-JdsBugKey([string]$Hex) { if (-not $Hex) { return '' } try { return ('0x{0:X8}' -f [uint32]$Hex) } catch { return $Hex.ToUpperInvariant() } } function Get-JdsBugHelp([string]$Hex) { $key = ConvertTo-JdsBugKey $Hex $cat = Get-JdsBugCheckCatalog $hit = $cat[$key] $learn = 'https://learn.microsoft.com/windows-hardware/drivers/debugger/bug-check-code-reference2' if ($hit) { return "Stop $key — $($hit.Name)`r`n`r`n$($hit.Tips)`r`n`r`nActions JDS : SFC, DISM, chkdsk, mdsched, désactiver démarrage rapide, MAJ pilotes.`r`nDoc Microsoft : $learn" } if ($key) { return "Stop $key (non catalogué).`r`nPistes génériques : SFC + DISM, chkdsk, mdsched, Minidump, MAJ Windows/pilotes, désactiver le dernier matériel.`r`nDoc Microsoft : $learn" } return 'Sélectionne un BSOD ou lance Actualiser.' } function Get-JdsHealthHints { $findings = New-Object System.Collections.Generic.List[string] $sys = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'" -ErrorAction SilentlyContinue if ($sys -and $sys.Size -gt 0) { $pct = [math]::Round(100 * $sys.FreeSpace / $sys.Size, 0) if ($pct -lt 10) { $findings.Add((New-JdsFinding 'CRITIQUE' 'Disque C: presque plein' "Il reste $pct % d'espace libre sur C:" 'SFC, DISM, Windows Update et les MAJ Office peuvent échouer' 'Libérer de l''espace (TEMP, corbeille, gros fichiers) avant toute réparation')) } elseif ($pct -lt 20) { $findings.Add((New-JdsFinding 'ATTENTION' 'Espace C: faible' "Seulement $pct % libres" 'Risque d''échec des mises à jour et de l''installation de logiciels' 'Surveiller ; viser > 15 % avant gros travaux')) } } $bc = Get-JdsEventsSafe -LogName System -Id 1001 -Max 3 -Days 90 if ($bc.Count) { $hex = '' if ($bc[0].Message -match '0x[0-9A-Fa-f]+') { $hex = $Matches[0] } $key = ConvertTo-JdsBugKey $hex $name = (Get-JdsBugCheckCatalog)[$key].Name $findings.Add((New-JdsFinding 'CRITIQUE' "BSOD récent ($key)" 'Plantage noyau Windows (BugCheck)' 'PC instable, perte de travail, risque matériel ou pilote' "Voir onglet Incidents → fiche $name ; SFC + DISM + chkdsk ; MAJ pilotes")) } if (Test-Path 'C:\Windows\Minidump') { $recentDmp = @(Get-ChildItem 'C:\Windows\Minidump\*.dmp' -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -gt (Get-Date).AddDays(-30) }) if ($recentDmp.Count -and -not $bc.Count) { $findings.Add((New-JdsFinding 'ATTENTION' "$($recentDmp.Count) minidump(s) récent(s)" 'Dumps présents sans Event 1001 récent listé' 'Trace d''un plantage passé' 'Onglet Incidents → Ouvrir Minidump')) } } $k41 = Get-JdsEventsSafe -LogName System -Id 41 -Max 3 -Days 30 if ($k41.Count) { $findings.Add((New-JdsFinding 'ATTENTION' 'Extinction inattendue (Event 41)' 'Coupure brutale sans arrêt propre (alim, crash, bouton power, BSOD)' 'Fichiers corrompus, volumes dirty, applications instables' 'Vérifier alimentation ; chkdsk si dirty ; onglet Incidents')) } Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' -ErrorAction SilentlyContinue | ForEach-Object { $letter = $_.DeviceID.TrimEnd('\') $q = cmd /c "fsutil dirty query $letter" 2>$null | Out-String if ($q -match 'is dirty' -or $q -match 'est sale') { $findings.Add((New-JdsFinding 'CRITIQUE' "Volume $letter marqué dirty" 'Arrêt brutal ou erreur disque / NTFS' 'chkdsk forcé au prochain boot ; risque de perte de données' "Onglet Disque → chkdsk $letter /f /r (reboot) après sauvegarde")) } } try { Get-PhysicalDisk -ErrorAction Stop | Where-Object { $_.HealthStatus -and $_.HealthStatus -ne 'Healthy' } | ForEach-Object { $findings.Add((New-JdsFinding 'CRITIQUE' "Disque $($_.FriendlyName) en $($_.HealthStatus)" 'SMART / contrôleur signale un problème matériel' 'Panne imminente, perte de données' 'Sauvegarder tout de suite ; remplacer le disque ; ne pas ignorer')) } } catch { } try { $badPnp = @(Get-PnpDevice -PresentOnly -ErrorAction Stop | Where-Object { $_.Status -eq 'Error' }) if ($badPnp.Count) { $names = ($badPnp | Select-Object -First 3 | ForEach-Object { $_.FriendlyName }) -join ', ' $findings.Add((New-JdsFinding 'ATTENTION' "$($badPnp.Count) périphérique(s) en erreur" "Pilote manquant, désactivé ou en conflit ($names…)" 'Fonctionnalité HS (Wi-Fi, USB, audio…)' 'Gestionnaire de périphériques → onglet Périphériques ; réinstaller pilote')) } } catch { } $pending = $false foreach ($p in @( 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired', 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending' )) { if (Test-Path $p) { $pending = $true } } if ($pending) { $findings.Add((New-JdsFinding 'ATTENTION' 'Redémarrage en attente' 'MAJ Windows ou réparation en cours de finalisation' 'Réparations incomplètes tant que le PC n''a pas reboot' 'Planifier un redémarrage avant de clôturer le ticket')) } $wuFail = Get-JdsEventsSafe -LogName 'Microsoft-Windows-WindowsUpdateClient/Operational' -Id 20,24,25,31,34 -Max 5 -Days 14 if ($wuFail.Count) { $findings.Add((New-JdsFinding 'ATTENTION' 'Échecs Windows Update récents' 'Cache WU corrompu, espace disque, ou image Windows endommagée' 'Correctifs de sécurité non installés' 'Onglet Réparer → Reset WU puis DISM RestoreHealth')) } $critNames = @('wuauserv', 'bits', 'cryptsvc', 'TrustedInstaller', 'spooler', 'WSearch', 'Dhcp', 'Dnscache') $stoppedCrit = @() foreach ($n in $critNames) { $svc = Get-Service -Name $n -ErrorAction SilentlyContinue if ($svc -and $svc.StartType -eq 'Automatic' -and $svc.Status -ne 'Running') { $stoppedCrit += $svc.Name } } if ($stoppedCrit.Count) { $findings.Add((New-JdsFinding 'ATTENTION' 'Services critiques arrêtés' (($stoppedCrit -join ', ')) 'MAJ, impression, DNS ou recherche peuvent échouer' 'Onglet Système → liste services ; Réparer selon le service')) } $memFail = Get-JdsEventsSafe -LogName System -Max 5 -Days 730 -Provider 'Microsoft-Windows-MemoryDiagnostics-Results' $memBad = @($memFail | Where-Object { $_.Message -match '(?i)fail|échec|hardware|matériel|error|erreur' }) if ($memBad.Count) { $findings.Add((New-JdsFinding 'CRITIQUE' 'Diagnostic mémoire en échec' 'mdsched a détecté des erreurs RAM' 'BSOD MEMORY_MANAGEMENT, corruption, plantages aléatoires' 'Remplacer / reseater la RAM ; retirer XMP ; retester mdsched')) } $hb = 0 try { $hb = [int](Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power' -Name HiberbootEnabled -ErrorAction SilentlyContinue).HiberbootEnabled } catch { } if ($hb -eq 1 -and ($bc.Count -gt 0 -or $k41.Count -gt 0)) { $findings.Add((New-JdsFinding 'ATTENTION' 'Démarrage rapide + incidents récents' 'Fast Startup garde le noyau en hibernation' 'BSOD et pilotes peuvent sembler « non résolus » après reboot' 'Onglet Réparer → Désactiver démarrage rapide')) } try { $net = Get-JdsNetwork $p = $net.Primary if ($p) { if ($p.IpMode -match 'fixe|manuel') { $findings.Add((New-JdsFinding 'INFO' 'IP fixe détectée' "Connexion $($p.Alias) en adressage statique ($($p.IPv4))" 'Reset winsock / pile IP / changement de carte = perte réseau' 'Noter IP, masque, passerelle, DNS avant toute réparation réseau')) } if ($p.DnsMode -eq 'Manuel') { $findings.Add((New-JdsFinding 'INFO' 'DNS manuel' "DNS configurés à la main : $($p.Dns)" 'Flush DNS ou reset IP peut casser la résolution de noms' 'Documenter les DNS ; éviter reset pile IP sans accord client')) } if ($net.MappedDrives.Count -gt 0) { $letters = ($net.MappedDrives | ForEach-Object { $_.Letter }) -join ', ' $findings.Add((New-JdsFinding 'INFO' 'Lecteurs réseau mappés' "$($net.MappedDrives.Count) partage(s) : $letters" 'Logiciels métier peuvent dépendre de ces lecteurs' 'Ne pas déconnecter les lecteurs ; vérifier avant reset réseau')) } if ($net.PartOfDomain) { $findings.Add((New-JdsFinding 'INFO' 'PC joint au domaine' "Domaine $($net.Domain)" 'GPO peut imposer proxy, DNS, pare-feu' 'Éviter reset réseau agressif ; privilégier gpresult / support AD')) } } } catch { } if ($findings.Count -eq 0) { $findings.Add((New-JdsFinding 'OK' 'Rien de critique détecté' 'Scan rapide sans anomalie majeure' 'PC semble stable pour un diagnostic de base' 'Tu peux approfondir via Incidents / Disque si symptômes client')) } return ($findings -join "`r`n`r`n") } function Get-JdsIncidentsText { $sb = New-Object System.Text.StringBuilder [void]$sb.AppendLine('=== BSOD (System 1001) — cause → effet → action ===') $bcs = Get-JdsEventsSafe -LogName System -Id 1001 -Max 15 -Days 180 if ($bcs.Count -eq 0) { [void]$sb.AppendLine('(aucun BSOD sur la période)') } foreach ($e in $bcs) { $hex = '' if ($e.Message -match '0x[0-9A-Fa-f]+') { $hex = $Matches[0] } $key = ConvertTo-JdsBugKey $hex $hit = (Get-JdsBugCheckCatalog)[$key] $name = if ($hit) { $hit.Name } else { 'Inconnu' } [void]$sb.AppendLine(('{0:yyyy-MM-dd HH:mm} {1} {2}' -f $e.TimeCreated, $key, $name)) if ($hit) { [void]$sb.AppendLine(' Cause probable : pilote / matériel / corruption système (voir fiche ci-dessous)') [void]$sb.AppendLine(" Pistes : $($hit.Tips)") } else { [void]$sb.AppendLine(' Cause probable : à déterminer (minidump + SFC/DISM)') } [void]$sb.AppendLine(' Effet : arrêt brutal, travail perdu, instabilité') [void]$sb.AppendLine(' Action JDS : SFC → DISM → chkdsk → MAJ pilotes → mdsched si RAM suspectée') [void]$sb.AppendLine('') } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Minidumps ===') if (Test-Path 'C:\Windows\Minidump') { $dmp = @(Get-ChildItem 'C:\Windows\Minidump\*.dmp' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending) if ($dmp.Count -eq 0) { [void]$sb.AppendLine('(aucun fichier)') } foreach ($f in $dmp) { [void]$sb.AppendLine(('{0:yyyy-MM-dd HH:mm} {1:N1} Ko {2}' -f $f.LastWriteTime, ($f.Length / 1KB), $f.Name)) } } else { [void]$sb.AppendLine('(dossier Minidump absent)') } [void]$sb.AppendLine('=== Kernel-Power 41 — coupure brutale ===') $k = Get-JdsEventsSafe -LogName System -Id 41 -Max 12 -Days 90 if ($k.Count -eq 0) { [void]$sb.AppendLine('(aucune)') } foreach ($e in $k) { [void]$sb.AppendLine(('{0:yyyy-MM-dd HH:mm}' -f $e.TimeCreated)) [void]$sb.AppendLine(' Cause probable : coupure courant, bouton power, crash noyau, ou veille forcée') [void]$sb.AppendLine(' Effet : volumes dirty, apps corrompues, sync cloud interrompue') [void]$sb.AppendLine(' Action JDS : vérifier alim/UPS ; chkdsk si dirty ; voir BSOD même période') [void]$sb.AppendLine('') } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Plantages applications (Application 1000) ===') $apps = Get-JdsEventsSafe -LogName Application -Id 1000 -Max 20 -Days 30 if ($apps.Count -eq 0) { [void]$sb.AppendLine('(aucun)') } foreach ($e in $apps) { $m = (($e.Message -replace '\s+', ' ').Trim()) if ($m.Length -gt 200) { $m = $m.Substring(0, 200) + '…' } [void]$sb.AppendLine(('{0:yyyy-MM-dd HH:mm} {1}' -f $e.TimeCreated, $m)) } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== .NET Runtime (Application 1026) ===') $dotnet = Get-JdsEventsSafe -LogName Application -Id 1026 -Max 15 -Days 30 if ($dotnet.Count -eq 0) { [void]$sb.AppendLine('(aucun)') } foreach ($e in $dotnet) { $m = (($e.Message -replace '\s+', ' ').Trim()) if ($m.Length -gt 200) { $m = $m.Substring(0, 200) + '…' } [void]$sb.AppendLine(('{0:yyyy-MM-dd HH:mm} {1}' -f $e.TimeCreated, $m)) } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Reliability (si WMI dispo) ===') try { $rel = @(Get-CimInstance -Namespace 'root/cimv2' -ClassName Win32_ReliabilityRecords -ErrorAction Stop | Select-Object -First 15) if ($rel.Count -eq 0) { [void]$sb.AppendLine('(vide)') } foreach ($r in $rel) { [void]$sb.AppendLine(('{0} {1} {2}' -f $r.TimeGenerated, $r.EventIdentifier, $r.ProductName)) } } catch { [void]$sb.AppendLine('(classe Reliability absente sur ce PC)') } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Diagnostic mémoire ===') $mem = Get-JdsEventsSafe -LogName System -Max 8 -Days 730 -Provider 'Microsoft-Windows-MemoryDiagnostics-Results' if ($mem.Count -eq 0) { [void]$sb.AppendLine('(aucun résultat mdsched)') } foreach ($e in $mem) { [void]$sb.AppendLine(('{0:yyyy-MM-dd HH:mm} {1}' -f $e.TimeCreated, (($e.Message -replace '\s+', ' ').Trim()))) } $sb.ToString() } function Get-JdsDiskToolsText { $sb = New-Object System.Text.StringBuilder [void]$sb.AppendLine('=== Volumes + dirty ===') Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' -ErrorAction SilentlyContinue | ForEach-Object { $letter = $_.DeviceID.TrimEnd('\') $free = [math]::Round($_.FreeSpace / 1GB, 1) $size = [math]::Round($_.Size / 1GB, 1) $q = cmd /c "fsutil dirty query $letter" 2>$null | Out-String $dirty = 'clean' if ($q -match 'is dirty' -or $q -match 'est sale') { $dirty = 'DIRTY' } [void]$sb.AppendLine(('{0} {1} {2}/{3} Go libres {4}' -f $letter, $_.FileSystem, $free, $size, $dirty)) } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== SMART / PhysicalDisk ===') try { Get-PhysicalDisk -ErrorAction Stop | ForEach-Object { [void]$sb.AppendLine(('{0} {1} santé={2} {3} Go {4}' -f $_.FriendlyName, $_.MediaType, $_.HealthStatus, [math]::Round($_.Size / 1GB, 0), $_.BusType)) } Get-PhysicalDisk -ErrorAction SilentlyContinue | ForEach-Object { try { $c = $_ | Get-StorageReliabilityCounter -ErrorAction Stop [void]$sb.AppendLine((' temp={0}°C readErr={1} writeErr={2} wear={3}' -f $c.Temperature, $c.ReadErrorsTotal, $c.WriteErrorsTotal, $c.Wear)) } catch { } } } catch { [void]$sb.AppendLine($_.Exception.Message) } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Journal disque (7, 51, 55, 153) ===') $de = Get-JdsEventsSafe -LogName System -Id 7,51,55,153 -Max 20 -Days 60 if ($de.Count -eq 0) { [void]$sb.AppendLine('(aucune)') } foreach ($e in $de) { $m = (($e.Message -replace '\s+', ' ').Trim()) if ($m.Length -gt 180) { $m = $m.Substring(0, 180) + '…' } [void]$sb.AppendLine(('{0:yyyy-MM-dd HH:mm} id={1} {2}' -f $e.TimeCreated, $e.Id, $m)) } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Espaces Windows (approx., sans WinSxS) ===') foreach ($p in @("$env:SystemRoot\Temp", "$env:TEMP", "$env:SystemRoot\Logs\CBS")) { if (Test-Path $p) { $bytes = (Get-ChildItem $p -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum [void]$sb.AppendLine(('{0:N1} Mo {1}' -f ($bytes / 1MB), $p)) } } [void]$sb.AppendLine('WinSxS : trop lourd à mesurer ici — utiliser DISM StartComponentCleanup.') [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Batterie ===') $bat = @(Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue) if ($bat.Count -eq 0) { [void]$sb.AppendLine('(pas de batterie WMI — PC fixe ?)') } foreach ($b in $bat) { [void]$sb.AppendLine(('{0} charge={1}% statut={2}' -f $b.Name, $b.EstimatedChargeRemaining, $b.BatteryStatus)) } $sb.ToString() } function Get-JdsDevicesText { $sb = New-Object System.Text.StringBuilder [void]$sb.AppendLine('=== Périphériques en erreur ===') try { $bad = @(Get-PnpDevice -ErrorAction Stop | Where-Object { $_.Status -eq 'Error' -or ($_.ConfigManagerErrorCode -and $_.ConfigManagerErrorCode -ne 0) }) if ($bad.Count -eq 0) { [void]$sb.AppendLine('(aucun)') } foreach ($d in $bad) { [void]$sb.AppendLine(('{0} [{1}] {2} code={3}' -f $d.FriendlyName, $d.Class, $d.Status, $d.ConfigManagerErrorCode)) } } catch { [void]$sb.AppendLine($_.Exception.Message) } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Pilotes (40 plus récents) ===') try { $dr = @(Get-CimInstance Win32_PnPSignedDriver -ErrorAction Stop | Sort-Object DriverDate -Descending | Select-Object -First 40) foreach ($d in $dr) { $sig = if ($d.IsSigned) { 'signé' } else { 'NON SIGNÉ' } [void]$sb.AppendLine(('{0:yyyy-MM-dd} {1} {2} {3} {4}' -f $d.DriverDate, $d.DeviceName, $d.DriverVersion, $sig, $d.Manufacturer)) } } catch { [void]$sb.AppendLine($_.Exception.Message) } $sb.ToString() } function Get-JdsSystemToolsText { $sb = New-Object System.Text.StringBuilder [void]$sb.AppendLine('=== Démarrage rapide / hibernation ===') try { $hb = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power' -Name HiberbootEnabled -ErrorAction SilentlyContinue).HiberbootEnabled [void]$sb.AppendLine("HiberbootEnabled = $hb (1 = démarrage rapide ON)") } catch { } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Secure Boot / TPM ===') try { [void]$sb.AppendLine('Secure Boot = ' + (Confirm-SecureBootUEFI)) } catch { [void]$sb.AppendLine('Secure Boot : ' + $_.Exception.Message) } try { $tpm = Get-Tpm -ErrorAction Stop [void]$sb.AppendLine(('TPM Present={0} Ready={1} Enabled={2}' -f $tpm.TpmPresent, $tpm.TpmReady, $tpm.TpmEnabled)) } catch { [void]$sb.AppendLine('TPM : ' + $_.Exception.Message) } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== BitLocker (volumes) ===') try { Get-BitLockerVolume -ErrorAction Stop | ForEach-Object { [void]$sb.AppendLine(('{0} Protection={1} Status={2} %={3}' -f $_.MountPoint, $_.ProtectionStatus, $_.VolumeStatus, $_.EncryptionPercentage)) } } catch { [void]$sb.AppendLine('(BitLocker non dispo / cmdlet absente) ' + $_.Exception.Message) } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Pare-feu ===') try { Get-NetFirewallProfile -ErrorAction Stop | ForEach-Object { [void]$sb.AppendLine(('{0} enabled={1} inbound={2} outbound={3}' -f $_.Name, $_.Enabled, $_.DefaultInboundAction, $_.DefaultOutboundAction)) } } catch { [void]$sb.AppendLine($_.Exception.Message) } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Proxy WinHTTP ===') [void]$sb.AppendLine((netsh winhttp show proxy 2>&1 | Out-String).Trim()) try { $inet = Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -ErrorAction SilentlyContinue [void]$sb.AppendLine(('WinInet ProxyEnable={0} ProxyServer={1}' -f $inet.ProxyEnable, $inet.ProxyServer)) } catch { } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Programmes au démarrage (Run) ===') foreach ($rp in @( 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run', 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' )) { if (-not (Test-Path $rp)) { continue } [void]$sb.AppendLine("-- $rp") Get-ItemProperty $rp -ErrorAction SilentlyContinue | ForEach-Object { $_.PSObject.Properties | Where-Object { $_.Name -notmatch '^PS' } | ForEach-Object { [void]$sb.AppendLine((' {0} = {1}' -f $_.Name, $_.Value)) } } } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Points de restauration ===') try { Get-ComputerRestorePoint -ErrorAction Stop | Select-Object -Last 12 | ForEach-Object { [void]$sb.AppendLine(('{0} {1} {2}' -f $_.CreationTime, $_.SequenceNumber, $_.Description)) } } catch { [void]$sb.AppendLine($_.Exception.Message) } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Services auto arrêtés ===') Get-Service -ErrorAction SilentlyContinue | Where-Object { $_.StartType -eq 'Automatic' -and $_.Status -ne 'Running' } | Select-Object -First 30 | ForEach-Object { [void]$sb.AppendLine(('{0} {1} {2}' -f $_.Status, $_.Name, $_.DisplayName)) } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Tâches planifiées en échec ===') try { Get-ScheduledTask -ErrorAction Stop | ForEach-Object { $i = $_ | Get-ScheduledTaskInfo -ErrorAction SilentlyContinue if ($i -and $null -ne $i.LastTaskResult -and $i.LastTaskResult -ne 0 -and $i.LastTaskResult -ne 267009) { [void]$sb.AppendLine(('{0} last={1} {2}' -f $i.LastTaskResult, $i.LastRunTime, $_.TaskName)) } } } catch { [void]$sb.AppendLine($_.Exception.Message) } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Windows Update (échecs opérationnels) ===') $wu = Get-JdsEventsSafe -LogName 'Microsoft-Windows-WindowsUpdateClient/Operational' -Max 15 -Days 21 $wu = @($wu | Where-Object { $_.LevelDisplayName -match 'Error|Erreur|Critique|Critical' -or $_.Id -in 20,24,25,31,34 }) if ($wu.Count -eq 0) { [void]$sb.AppendLine('(pas d''échec listé)') } foreach ($e in $wu | Select-Object -First 15) { $m = (($e.Message -replace '\s+', ' ').Trim()) if ($m.Length -gt 180) { $m = $m.Substring(0, 180) + '…' } [void]$sb.AppendLine(('{0:yyyy-MM-dd HH:mm} {1}' -f $e.TimeCreated, $m)) } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== Event Viewer erreurs/critiques 7 j (System+App, 40 max) ===') $ev = @() try { $ev += @(Get-WinEvent -FilterHashtable @{ LogName = 'System'; Level = 1,2; StartTime = (Get-Date).AddDays(-7) } -MaxEvents 20 -ErrorAction Stop) } catch { } try { $ev += @(Get-WinEvent -FilterHashtable @{ LogName = 'Application'; Level = 1,2; StartTime = (Get-Date).AddDays(-7) } -MaxEvents 20 -ErrorAction Stop) } catch { } foreach ($e in $ev | Sort-Object TimeCreated -Descending | Select-Object -First 40) { $m = (($e.Message -replace '\s+', ' ').Trim()) if ($m.Length -gt 140) { $m = $m.Substring(0, 140) + '…' } [void]$sb.AppendLine(('{0:yyyy-MM-dd HH:mm} {1}/{2} {3}' -f $e.TimeCreated, $e.LogName, $e.Id, $m)) } [void]$sb.AppendLine('') [void]$sb.AppendLine('=== GPO (gpresult /R, extrait) ===') try { $gpr = gpresult /R 2>&1 | Out-String $lines = $gpr -split "`r?`n" $take = $lines | Select-Object -First 60 [void]$sb.AppendLine(($take -join "`r`n")) } catch { [void]$sb.AppendLine($_.Exception.Message) } $sb.ToString() } function Invoke-JdsWuReset { $names = @('wuauserv', 'bits', 'cryptsvc', 'msiserver') foreach ($n in $names) { Stop-Service -Name $n -Force -ErrorAction SilentlyContinue } Start-Sleep -Seconds 2 $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' foreach ($pair in @( @{ Path = "$env:SystemRoot\SoftwareDistribution"; Name = "SoftwareDistribution.bak-$stamp" }, @{ Path = "$env:SystemRoot\System32\catroot2"; Name = "catroot2.bak-$stamp" } )) { if (Test-Path $pair.Path) { Rename-Item -LiteralPath $pair.Path -NewName $pair.Name -ErrorAction SilentlyContinue } } foreach ($n in $names) { Start-Service -Name $n -ErrorAction SilentlyContinue } return 'Windows Update : caches renommés, services relancés. Relance une recherche de MAJ.' } function Clear-JdsPrefetch { $n = 0 Get-ChildItem -LiteralPath "$env:SystemRoot\Prefetch" -Force -ErrorAction SilentlyContinue | ForEach-Object { try { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction Stop; $n++ } catch { } } return $n } function Reset-JdsSpooler { Stop-Service -Name spooler -Force -ErrorAction SilentlyContinue Get-ChildItem -LiteralPath "$env:SystemRoot\System32\spool\PRINTERS" -Force -ErrorAction SilentlyContinue | ForEach-Object { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue } Start-Service -Name spooler -ErrorAction SilentlyContinue return 'Spooler réinitialisé (file d''attente vidée).' } function Repair-JdsStartMenu { $pkgs = Get-AppxPackage -ErrorAction SilentlyContinue | Where-Object { $_.Name -match 'ShellExperienceHost|StartMenuExperienceHost' } foreach ($p in $pkgs) { $man = Join-Path $p.InstallLocation 'AppXManifest.xml' if (Test-Path $man) { Add-AppxPackage -DisableDevelopmentMode -Register $man -ErrorAction SilentlyContinue | Out-Null } } return 'Re-register Start/ShellExperienceHost lancé.' } function Get-JdsHtmlReport($Data) { function Enc([string]$s) { [System.Net.WebUtility]::HtmlEncode($s) } function Pre($text) { '
' + (Enc ([string]$text)) + '
' } $os = $Data.Os $lic = $Data.License $winOk = if ($lic.Licensed) { 'ok' } else { 'ko' } $offOk = if ($Data.Office.Found) { 'ok' } else { 'ko' } $offBadge = if ($Data.Office.Found) { $Data.Office.DisplayName } else { 'Non détecté' } $when = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $appRows = ($Data.Apps | ForEach-Object { '{0}{1}{2}{3}{4}{5}{6}' -f (Enc $_.Name), (Enc $_.Version), (Enc $_.Publisher), (Enc $_.SizeDisplay), (Enc $_.BloatLabel), (Enc $_.InstallDate), (Enc $_.Scope) }) -join "`n" @" JDS — Rapport $($os.ComputerName)

JDS — Rapport technique

$($os.ComputerName) · $when · JDS $($script:JdsVersion)
Windows : $(Enc $lic.Status) Office : $(Enc $offBadge)

Windows

$(Pre (Format-JdsWindowsText $os $lic))

Office

$(Pre (Format-JdsOfficeText $Data.Office))

Matériel

$(Pre (Format-JdsHardwareText $Data.Hardware))

Réseau

$(Pre (Format-JdsNetworkText $Data.Network))

Disques

$(Pre (Format-JdsDisksText $Data.Disks))

Utilisateurs

$(Pre (Format-JdsUsersText $Data.Users))

Mises à jour

$(Pre (Format-JdsUpdatesText $Data.Updates)) $(if ($Data.HealthText) { "

Diagnostic

$(Pre $Data.HealthText)" }) $(if ($Data.IncidentsText) { "

Incidents

$(Pre $Data.IncidentsText)" }) $(if ($Data.DiskToolsText) { "

Disque (outils)

$(Pre $Data.DiskToolsText)" }) $(if ($Data.DevicesText) { "

Périphériques

$(Pre $Data.DevicesText)" }) $(if ($Data.SystemToolsText) { "

Système (outils)

$(Pre $Data.SystemToolsText)" })

Applications ($($Data.Apps.Count))

$appRows
NomVersionÉditeurTailleStatutInstalléPortée
"@ } # --------------------------------------------------------------------------- # Kit catalogue (Logiciels / Scripts / Modeles / Rapport) # --------------------------------------------------------------------------- function Get-JdsKitBundledCatalogDir { if ($PSCommandPath) { $sib = Join-Path (Split-Path $PSCommandPath) 'kit' if (Test-Path (Join-Path $sib 'Logiciels')) { return $sib } } $dev = Join-Path $PWD 'kit' if (Test-Path (Join-Path $dev 'Logiciels')) { return $dev } return $null } function Expand-JdsKitCatalogZip([string]$ZipPath, [string]$Dest) { if (-not (Test-Path -LiteralPath $ZipPath)) { return $false } New-Item -ItemType Directory -Path $Dest -Force | Out-Null Expand-Archive -LiteralPath $ZipPath -DestinationPath $Dest -Force return (Test-Path (Join-Path $Dest 'Logiciels')) } function Install-JdsKitCatalogSeed { param([string]$Dest) New-Item -ItemType Directory -Path $Dest -Force | Out-Null $bundled = Get-JdsKitBundledCatalogDir if ($bundled) { Copy-Item -Path (Join-Path $bundled '*') -Destination $Dest -Recurse -Force return $true } $seedB64 = $null if ($PSCommandPath) { $p = Join-Path (Split-Path $PSCommandPath) 'kit\seed.b64' if (Test-Path -LiteralPath $p) { $seedB64 = Get-Content -LiteralPath $p -Raw } } if (-not $seedB64 -and (Test-Path -LiteralPath (Join-Path $PWD 'kit\seed.b64'))) { $seedB64 = Get-Content -LiteralPath (Join-Path $PWD 'kit\seed.b64') -Raw } if ($seedB64) { $zip = Join-Path $env:TEMP 'jds-kit-catalog.zip' [IO.File]::WriteAllBytes($zip, [Convert]::FromBase64String($seedB64.Trim())) return (Expand-JdsKitCatalogZip $zip $Dest) } try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $zip = Join-Path $env:TEMP 'jds-kit-catalog.zip' Invoke-WebRequest -Uri $script:JdsKitCatalogUrl -OutFile $zip -UseBasicParsing -TimeoutSec 60 return (Expand-JdsKitCatalogZip $zip $Dest) } catch { return $false } } function Get-JdsKitCatalogRoot { if (-not (Test-Path (Join-Path $script:JdsKitCatalog 'Logiciels'))) { Install-JdsKitCatalogSeed -Dest $script:JdsKitCatalog | Out-Null } return $script:JdsKitCatalog } function ConvertFrom-JdsKitIni { param([string]$Path, [string]$Category) if (-not (Test-Path -LiteralPath $Path)) { return @() } $lines = Get-Content -LiteralPath $Path -Encoding UTF8 $items = @() $current = $null foreach ($raw in $lines) { $line = $raw.Trim() if (-not $line -or $line.StartsWith(';') -or $line.StartsWith('#')) { continue } if ($line -match '^\[(.+)\]$') { if ($current -and $current.Lien) { $items += [pscustomobject]$current } $current = [ordered]@{ Name = $Matches[1].Trim() Category = $Category Lien = '' Site = $false ForceDl = $false Extension = '' Favoris = $false NoUpdate = $false Headers = $false DisplayLabel = '' } continue } if (-not $current) { continue } if ($line -match '^(?i)lien\s*=\s*(.+)$') { $current.Lien = $Matches[1].Trim() } elseif ($line -match '^(?i)site\s*=\s*1') { $current.Site = $true } elseif ($line -match '^(?i)forcedl\s*=\s*1') { $current.ForceDl = $true } elseif ($line -match '^(?i)extension\s*=\s*(.+)$') { $current.Extension = $Matches[1].Trim().TrimStart('.') } elseif ($line -match '^(?i)favoris\s*=\s*1') { $current.Favoris = $true } elseif ($line -match '^(?i)nepasmaj\s*=\s*1') { $current.NoUpdate = $true } elseif ($line -match '^(?i)headers\s*=\s*1') { $current.Headers = $true } } if ($current -and $current.Lien) { $items += [pscustomobject]$current } foreach ($i in $items) { $tag = if ($i.Site) { 'site' } elseif ($i.Lien -match '^https?://') { 'dl' } else { 'cmd' } $fav = if ($i.Favoris) { '★ ' } else { '' } $i.DisplayLabel = "$fav$($i.Name) [$tag]" } @($items) } function Get-JdsKitSoftwareCatalog { $root = Get-JdsKitCatalogRoot $dir = Join-Path $root 'Logiciels' $all = @() if (-not (Test-Path $dir)) { return @() } Get-ChildItem -LiteralPath $dir -Filter '*.ini' | Sort-Object Name | ForEach-Object { $cat = [IO.Path]::GetFileNameWithoutExtension($_.Name) -replace '-', ' ' $all += ConvertFrom-JdsKitIni -Path $_.FullName -Category $cat } @($all | Sort-Object Category, Name) } function Get-JdsKitAppDir($Item) { $safeCat = ($Item.Category -replace '[^\w\- ]', '').Trim() $safeName = ($Item.Name -replace '[^\w\- \.]', '').Trim() if (-not $safeCat) { $safeCat = 'Divers' } if (-not $safeName) { $safeName = 'outil' } Join-Path $script:JdsKitApps "$safeCat\$safeName" } function Test-JdsKitAppCached($Item) { $dir = Get-JdsKitAppDir $Item if (-not (Test-Path $dir)) { return $false } return [bool]@(Get-ChildItem -LiteralPath $dir -File -ErrorAction SilentlyContinue | Where-Object { $_.Extension -match '\.(exe|msi|bat|cmd|ps1|zip|7z|msi)$' }) } function Get-JdsKitCachedExe($Item) { $dir = Get-JdsKitAppDir $Item $exes = @(Get-ChildItem -LiteralPath $dir -File -ErrorAction SilentlyContinue | Where-Object { $_.Extension -match '\.(exe|msi|bat|cmd|ps1)$' } | Sort-Object Length -Descending) if ($exes.Count) { return $exes[0].FullName } return $null } function Save-JdsKitDownload { param($Item, [string]$Url) $dir = Get-JdsKitAppDir $Item New-Item -ItemType Directory -Path $dir -Force | Out-Null if ($Item.NoUpdate -and (Test-JdsKitAppCached $Item)) { return (Get-JdsKitCachedExe $Item) } $ext = if ($Item.Extension) { $Item.Extension } else { $u = $Url.Split('?')[0] $e = [IO.Path]::GetExtension($u).TrimStart('.') if ($e) { $e } else { 'exe' } } $safe = ($Item.Name -replace '[^\w\-]', '_') $out = Join-Path $dir "$safe.$ext" try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $headers = @{ 'User-Agent' = 'JDS-Kit/2.0' } if ($Item.Headers) { $headers['Referer'] = 'https://www.nirsoft.net/' } Invoke-WebRequest -Uri $Url -OutFile $out -Headers $headers -UseBasicParsing -TimeoutSec 180 if ($ext -eq 'zip' -and (Test-Path $out)) { Expand-Archive -LiteralPath $out -DestinationPath $dir -Force -ErrorAction SilentlyContinue } $exe = Get-JdsKitCachedExe $Item if ($exe) { return $exe } if (Test-Path $out) { return $out } } catch { throw "Téléchargement échoué : $($_.Exception.Message)" } return $null } function Start-JdsKitItem { param($Item) if (-not $Item -or -not $Item.Lien) { throw 'Entrée invalide.' } $lien = [string]$Item.Lien if ($Item.Site) { Start-Process $lien | Out-Null Write-JdsConsoleLine "Site ouvert : $lien" return "Site ouvert : $lien" } if ($lien -match '^https?://') { Write-JdsConsoleLine "Téléchargement : $lien" $path = Save-JdsKitDownload -Item $Item -Url $lien if (-not $path) { throw 'Fichier introuvable après téléchargement.' } if ($path -match '\.(exe|msi|bat|cmd)$') { Start-Process -FilePath $path | Out-Null } elseif ($path -match '\.ps1$') { Start-Process powershell.exe -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $path) | Out-Null } else { Start-Process explorer.exe -ArgumentList $path | Out-Null } Write-JdsConsoleLine "Lancé (UI) : $path" return "Lancé : $path" } # Commande Windows / chemin local if ($lien -match '^\\\\') { Start-Process explorer.exe -ArgumentList $lien | Out-Null Write-JdsConsoleLine "Ouvert : $lien" return "Ouvert : $lien" } if ($lien -match '\.(msc|cpl)$' -or $lien -match '^(control|perfmon|taskmgr|cmd|msconfig|dfrgui|cleanmgr|diskmgmt)') { Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', 'start', '""', $lien) | Out-Null Write-JdsConsoleLine "Commande GUI : $lien" return "Commande : $lien" } Start-Process -FilePath $lien -ErrorAction Stop | Out-Null Write-JdsConsoleLine "Lancé : $lien" return "Lancé : $lien" } function Get-JdsKitScripts { $root = Get-JdsKitCatalogRoot $dir = Join-Path $root 'Scripts' if (-not (Test-Path $dir)) { return @() } $list = @() Get-ChildItem -LiteralPath $dir -Recurse -File | Where-Object { $_.Extension -match '\.(bat|cmd|ps1|reg)$' } | Sort-Object FullName | ForEach-Object { $rel = $_.FullName.Substring($dir.Length).TrimStart('\') $list += [pscustomobject]@{ Name = $_.BaseName RelativePath = $rel FullPath = $_.FullName DisplayLabel = $rel } } $list } function Start-JdsKitScript { param($Script) if (-not $Script) { return } $p = $Script.FullPath if ($p -match '\.reg$') { if (-not (Confirm-Jds "Importer le fichier REG ?`r`n$p" 'JDS Kit')) { return 'Annulé.' } Start-Process regedit.exe -ArgumentList @('/s', $p) | Out-Null Write-JdsConsoleLine "REG importé : $p" return "REG importé : $p" } if ($p -match '\.ps1$') { if (-not (Confirm-Jds "Exécuter le script PowerShell ?`r`n$($Script.RelativePath)" 'JDS Kit')) { return 'Annulé.' } Start-JdsLiveProcess -FileName 'powershell.exe' -Arguments "-NoProfile -ExecutionPolicy Bypass -File `"$p`"" -WorkingDirectory (Split-Path $p) -Log "PS1 : $($Script.RelativePath)" return "PS1 lancé (console) : $($Script.RelativePath)" } if (-not (Confirm-Jds "Exécuter ?`r`n$($Script.RelativePath)" 'JDS Kit')) { return 'Annulé.' } if ($p -match '\.(bat|cmd)$') { Start-JdsLiveProcess -FileName 'cmd.exe' -Arguments "/c `"$p`"" -WorkingDirectory (Split-Path $p) -Log "Script : $($Script.RelativePath)" return "Script lancé (console) : $($Script.RelativePath)" } Start-Process -FilePath $p -WorkingDirectory (Split-Path $p) | Out-Null Write-JdsConsoleLine "Script lancé : $($Script.RelativePath)" return "Script lancé : $($Script.RelativePath)" } function Get-JdsKitModeles { $root = Get-JdsKitCatalogRoot $dir = Join-Path $root 'Modeles' if (-not (Test-Path $dir)) { return @() } @(Get-ChildItem -LiteralPath $dir -File | Sort-Object Name | ForEach-Object { [pscustomobject]@{ Name = $_.BaseName FullPath = $_.FullName DisplayLabel = $_.Name } }) } function New-JdsKitReportFolder { $desk = [Environment]::GetFolderPath('Desktop') $hostName = $env:COMPUTERNAME $name = 'JDS-Rapport-{0}-{1}' -f $hostName, (Get-Date -Format 'yyyyMMdd-HHmm') $path = Join-Path $desk $name New-Item -ItemType Directory -Path $path -Force | Out-Null $root = Get-JdsKitCatalogRoot $models = Join-Path $root 'Modeles' if (Test-Path $models) { Copy-Item (Join-Path $models '*') -Destination $path -Force -ErrorAction SilentlyContinue } $copy = Join-Path $root 'A-copier' if (Test-Path $copy) { Copy-Item (Join-Path $copy '*') -Destination $path -Force -ErrorAction SilentlyContinue } $note = @" JDS Kit — dossier rapport Poste : $hostName Date : $(Get-Date -Format 'dd/MM/yyyy HH:mm') Tech : $env:USERNAME Compléter les modèles .txt puis joindre à la fiche atelier https://dash.informatiquejds.com "@ [IO.File]::WriteAllText((Join-Path $path '00-Notes-JDS.txt'), $note, (New-Object System.Text.UTF8Encoding $false)) Start-Process explorer.exe $path | Out-Null return $path } # --------------------------------------------------------------------------- # UI # --------------------------------------------------------------------------- $xaml = @'