Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## Version 0.3.1 - 7/7/2026

### Bug Fixes
- **App Crash After Group Tag Update on Windows PowerShell 5.1** (Issue #64): The toast auto-dismiss timer handler was created with `GetNewClosure()`, which binds it to a dynamic module that cannot resolve script-scope functions on Windows PowerShell 5.1. Four seconds after any toast appeared (for example after a successful group tag update), the timer tick failed with "The term 'Hide-Toast' is not recognized" and took down the main window. The Graph update itself succeeded; only the UI crashed.
- **Autopilot Playbooks Fail with HTTP 500** (Issue #65): The Microsoft Graph `windowsAutopilotDeviceIdentities` endpoint currently returns an internal server error whenever a `$select` projection is used (verified on both beta and v1.0, regardless of the selected properties). The "Autopilot Devices Not in Intune" and "Intune Devices Not in Autopilot" playbooks now fetch the devices without `$select`, matching the dashboard behavior that was unaffected.
- **Latent Crashes from `$script:` Variables Inside Closures**: `GetNewClosure()` handlers read `$script:` variables as frozen snapshots (or null when nested), on all PowerShell versions. This broke the stale-toast protection check, made the "Copy Key" button reset timer throw on a null timer 2 seconds after copying a recovery key, and made the Entra ID / Disable in Entra ID checkbox mutual exclusivity handlers crash the offboarding dialog. All event handlers now use plain scriptblocks (which stay bound to the script session state) and reserve `GetNewClosure()` for capturing per-invocation locals.

## Version 0.3.0 - 7/7/2026

> **Note**: This is the final release of the PowerShell script. Version 0.4 will be a native Windows app (WinUI 3) that replaces the script. See [Issue #60](https://github.com/ugurkocde/DeviceOffboardingManager/issues/60).
Expand Down
40 changes: 26 additions & 14 deletions DeviceOffboardingManager.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<#PSScriptInfo

.VERSION 0.3.0
.VERSION 0.3.1

.GUID a686724d-588d-472e-b927-c4840c32eed1

Expand Down Expand Up @@ -4735,10 +4735,16 @@ $CollapsedStatusDot = $Window.FindName('CollapsedStatusDot')
$script:SidebarCollapsed = $false

$script:ToastGeneration = 0
$script:ToastHideGeneration = 0

# NOTE (Issue #64): toast handlers must be plain scriptblocks. GetNewClosure() binds a
# scriptblock to a dynamic module that cannot resolve script-scope functions on Windows
# PowerShell 5.1 ('Hide-Toast' is not recognized) and reads $script: variables as frozen
# snapshots instead of live values. Plain scriptblocks stay bound to the script session
# state; GetNewClosure() is only for capturing per-invocation locals.
function Hide-Toast {
$script:ToastGeneration++
$gen = $script:ToastGeneration
$script:ToastHideGeneration = $script:ToastGeneration
if ($script:ToastTimer) { $script:ToastTimer.Stop() }
# Clear current animation hold and read actual position
$ToastNotification.RenderTransform.BeginAnimation(
Expand All @@ -4751,7 +4757,7 @@ function Hide-Toast {
$slideOut.Duration = [System.Windows.Duration]::new([TimeSpan]::FromMilliseconds(200))
$slideOut.EasingFunction = New-Object System.Windows.Media.Animation.QuadraticEase
$slideOut.EasingFunction.EasingMode = 'EaseIn'
$slideOut.Add_Completed({ if ($script:ToastGeneration -eq $gen) { $ToastNotification.Visibility = 'Collapsed' } }.GetNewClosure())
$slideOut.Add_Completed({ if ($script:ToastGeneration -eq $script:ToastHideGeneration) { $ToastNotification.Visibility = 'Collapsed' } })
$ToastNotification.RenderTransform.BeginAnimation(
[System.Windows.Media.TranslateTransform]::YProperty, $slideOut)
}
Expand Down Expand Up @@ -4786,7 +4792,7 @@ function Show-Toast {
# Auto-dismiss timer
$script:ToastTimer = New-Object System.Windows.Threading.DispatcherTimer
$script:ToastTimer.Interval = [TimeSpan]::FromSeconds($DurationSeconds)
$script:ToastTimer.Add_Tick({ Hide-Toast }.GetNewClosure())
$script:ToastTimer.Add_Tick({ Hide-Toast })
$script:ToastTimer.Start()
}

Expand Down Expand Up @@ -5765,15 +5771,17 @@ $OffboardButton.Add_Click({
Write-Log "Failed to copy recovery key to clipboard: $_" -Severity "WARN"
$button.Content = "Copy failed"
}
$script:copyButtonTimer = New-Object System.Windows.Threading.DispatcherTimer
$script:copyButtonTimer.Interval = [TimeSpan]::FromSeconds(2)
$script:copyButtonTimer.Add_Tick({
# Local variable, not $script:, so GetNewClosure() captures it;
# $script: variables read as null inside nested closures (Issue #64).
$timer = New-Object System.Windows.Threading.DispatcherTimer
$timer.Interval = [TimeSpan]::FromSeconds(2)
$timer.Add_Tick({
$button.Content = "Copy Key"
$script:copyButtonTimer.Stop()
$timer.Stop()
}.GetNewClosure())
$script:copyButtonTimer.Start()
$timer.Start()
}
}.GetNewClosure()
}

$encryptionKeysList = $confirmationWindow.FindName('EncryptionKeysList')
$encryptionKeysList.AddHandler(
Expand Down Expand Up @@ -5842,12 +5850,14 @@ $OffboardButton.Add_Click({
}

# Mutual exclusivity: "Entra ID" (delete) vs "Disable in Entra ID"
# Plain scriptblocks: GetNewClosure() would read $script:serviceCheckboxes as null
# here because closures only capture per-invocation locals (Issue #64).
$script:serviceCheckboxes["Entra ID"].Add_Checked({
$script:serviceCheckboxes["Disable in Entra ID"].IsChecked = $false
}.GetNewClosure())
})
$script:serviceCheckboxes["Disable in Entra ID"].Add_Checked({
$script:serviceCheckboxes["Entra ID"].IsChecked = $false
}.GetNewClosure())
})

# Add button handlers
$cancelButton.Add_Click({
Expand Down Expand Up @@ -8656,7 +8666,8 @@ function Get-AutopilotNotIntuneDevices {
try {
# Get all Autopilot devices
Write-Host "Fetching Autopilot devices..." -ForegroundColor Cyan
$uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities?`$select=id,displayName,serialNumber,lastContactedDateTime,model,manufacturer"
# No $select: the Autopilot endpoint returns HTTP 500 for any $select projection (Issue #65)
$uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities"
$autopilotDevices = Get-GraphPagedResults -Uri $uri
Write-Host "Found $($autopilotDevices.Count) Autopilot devices" -ForegroundColor Green

Expand Down Expand Up @@ -8718,7 +8729,8 @@ function Get-IntuneNotAutopilotDevices {
try {
# Get all Autopilot devices
Write-Host "Fetching Autopilot devices..." -ForegroundColor Cyan
$uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities?`$select=serialNumber"
# No $select: the Autopilot endpoint returns HTTP 500 for any $select projection (Issue #65)
$uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities"
$autopilotDevices = Get-GraphPagedResults -Uri $uri
Write-Host "Found $($autopilotDevices.Count) Autopilot devices" -ForegroundColor Green

Expand Down
3 changes: 2 additions & 1 deletion Playbooks/Playbook_1.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ function Get-AutopilotNotIntuneDevices {
try {
# Get all Autopilot devices
Write-Host "Fetching Autopilot devices..." -ForegroundColor Cyan
$uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities?`$select=id,displayName,serialNumber,lastContactedDateTime,model,manufacturer"
# No $select: the Autopilot endpoint returns HTTP 500 for any $select projection (Issue #65)
$uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities"
$autopilotDevices = Get-GraphPagedResults -Uri $uri
Write-Host "Found $($autopilotDevices.Count) Autopilot devices" -ForegroundColor Green

Expand Down
3 changes: 2 additions & 1 deletion Playbooks/Playbook_2.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ function Get-IntuneNotAutopilotDevices {
try {
# Get all Autopilot devices
Write-Host "Fetching Autopilot devices..." -ForegroundColor Cyan
$uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities?`$select=serialNumber"
# No $select: the Autopilot endpoint returns HTTP 500 for any $select projection (Issue #65)
$uri = "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeviceIdentities"
$autopilotDevices = Get-GraphPagedResults -Uri $uri
Write-Host "Found $($autopilotDevices.Count) Autopilot devices" -ForegroundColor Green

Expand Down
Loading