Public/Show-WindowsDeviceLink.ps1

function Show-WindowsDeviceLink {
    <#
    .SYNOPSIS
    Opens the WindowsDeviceLink operator dashboard.
 
    .DESCRIPTION
    Opens a compact Windows 11 Settings-inspired WinForms dashboard on full Windows.
 
    The GUI focuses on inspecting Device Association state, onboarding, and offboarding.
    Lifecycle actions delegate to existing WindowsDeviceLink public cmdlets.
 
    Interactive authentication is the default. Use -Method and the corresponding authentication
    parameters to select another supported authentication flow.
 
    Use -Tenants to provide friendly tenant names for the tenant selector:
 
    @{'Management'='11111111-1111-1111-1111-111111111111'; 'Contoso'='22222222-2222-2222-2222-222222222222'}
 
    .EXAMPLE
    Show-WindowsDeviceLink
 
    .EXAMPLE
    Show-WindowsDeviceLink -Method DeviceCode
 
    .EXAMPLE
    Show-WindowsDeviceLink -Tenants @{
        'Management' = '11111111-1111-1111-1111-111111111111'
        'Contoso' = '22222222-2222-2222-2222-222222222222'
    }
    #>

    [CmdletBinding()]
    param(
        [ValidateSet(
            'DeviceCode','Interactive','ClientSecret','AccessToken','Certificate',
            'CertificateThumbprint','CertificateSubjectName','EnvironmentVariable','ManagedIdentity'
        )]
        [string]$Method = 'Interactive',

        [ValidateNotNullOrEmpty()]
        [string]$TenantId,

        [hashtable]$Tenants,

        [ValidateNotNullOrEmpty()]
        [string]$ClientId,

        [securestring]$AccessToken,

        [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,

        [ValidateNotNullOrEmpty()]
        [string]$CertificateThumbprint,

        [ValidateNotNullOrEmpty()]
        [string]$CertificateSubjectName,

        [bool]$SendCertificateChain = $false,

        [securestring]$ClientSecret,

        [ValidateNotNullOrEmpty()]
        [string]$Environment = 'Global',

        [ValidateRange(1,600)]
        [double]$ClientTimeout = 100
    )

    if (Test-Path -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\MiniNT') {
        throw 'Show-WindowsDeviceLink is not supported in Windows PE yet. Use the WindowsDeviceLink command-line cmdlets instead.'
    }

    $outerBoundParameters = @{}
    foreach ($key in $PSBoundParameters.Keys) {
        $outerBoundParameters[$key] = $PSBoundParameters[$key]
    }

    Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop
    Add-Type -AssemblyName System.Drawing -ErrorAction Stop
    [System.Windows.Forms.Application]::EnableVisualStyles()

    $loadedModule = Get-Module WindowsDeviceLink | Select-Object -First 1
    $baseVersion = if ($loadedModule) { $loadedModule.Version.ToString() } else { 'unknown' }
    $prerelease = $null
    if ($loadedModule -and $loadedModule.PrivateData -and $loadedModule.PrivateData.PSData) {
        $prerelease = [string]$loadedModule.PrivateData.PSData.Prerelease
    }
    $displayVersion = if ([string]::IsNullOrWhiteSpace($prerelease)) { $baseVersion } else { "$baseVersion-$prerelease" }

    $workingArea = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea
    $targetWidth = [Math]::Max(820,[Math]::Min(1080,$workingArea.Width - 32))
    $targetHeight = [Math]::Max(620,[Math]::Min(760,$workingArea.Height - 40))

    $form = New-Object System.Windows.Forms.Form
    $form.Text = "WindowsDeviceLink $displayVersion (Preview)"
    $form.StartPosition = 'CenterScreen'
    $form.Size = [System.Drawing.Size]::new($targetWidth,$targetHeight)
    $form.MinimumSize = [System.Drawing.Size]::new(820,620)
    $form.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::Dpi
    $form.BackColor = [System.Drawing.Color]::FromArgb(243,243,243)

    $content = New-Object System.Windows.Forms.Panel
    $content.Dock = [System.Windows.Forms.DockStyle]::Fill
    $content.AutoScroll = $false
    $content.BackColor = $form.BackColor
    $form.Controls.Add($content)

    $toolTip = New-Object System.Windows.Forms.ToolTip
    $toolTip.AutoPopDelay = 10000
    $toolTip.InitialDelay = 400
    $toolTip.ReshowDelay = 200
    $toolTip.ShowAlways = $true


    function New-Card {
        param(
            [string]$Title,
            [int]$X,
            [int]$Y,
            [int]$Width,
            [int]$Height
        )

        $panel = New-Object System.Windows.Forms.Panel
        $panel.Location = [System.Drawing.Point]::new($X,$Y)
        $panel.Size = [System.Drawing.Size]::new($Width,$Height)
        $panel.BackColor = [System.Drawing.Color]::White
        $panel.BorderStyle = [System.Windows.Forms.BorderStyle]::None
        $content.Controls.Add($panel)

        if ($Title) {
            $label = New-Object System.Windows.Forms.Label
            $label.Text = $Title
            $label.Font = New-Object System.Drawing.Font('Segoe UI',10,[System.Drawing.FontStyle]::Bold)
            $label.Location = [System.Drawing.Point]::new(14,9)
            $label.AutoSize = $true
            $panel.Controls.Add($label)
        }

        $panel
    }

    function New-ValuePair {
        param(
            [System.Windows.Forms.Control]$Parent,
            [string]$Caption,
            [int]$Y,
            [int]$CaptionWidth = 112
        )

        $captionLabel = New-Object System.Windows.Forms.Label
        $captionLabel.Text = $Caption
        $captionLabel.Font = New-Object System.Drawing.Font('Segoe UI',8.5)
        $captionLabel.ForeColor = [System.Drawing.Color]::FromArgb(102,102,102)
        $captionLabel.Location = [System.Drawing.Point]::new(14,$Y)
        $captionLabel.Size = [System.Drawing.Size]::new($CaptionWidth,20)
        $Parent.Controls.Add($captionLabel)

        $valueLabel = New-Object System.Windows.Forms.Label
        $valueLabel.Text = '-'
        $valueLabel.Font = New-Object System.Drawing.Font('Segoe UI',9,[System.Drawing.FontStyle]::Bold)
        $valueLabel.Location = [System.Drawing.Point]::new(($CaptionWidth + 20),($Y - 1))
        $valueLabel.Size = [System.Drawing.Size]::new(350,22)
        $valueLabel.AutoEllipsis = $true
        $Parent.Controls.Add($valueLabel)

        $valueLabel
    }

    function New-ActionRow {
        param(
            [System.Windows.Forms.Control]$Parent,
            [string]$Title,
            [string]$Description,
            [int]$Y,
            [string[]]$Buttons
        )

        $row = New-Object System.Windows.Forms.Panel
        $row.Location = [System.Drawing.Point]::new(0,$Y)
        $row.Size = [System.Drawing.Size]::new(900,46)
        $row.BackColor = [System.Drawing.Color]::White
        $Parent.Controls.Add($row)

        $titleLabel = New-Object System.Windows.Forms.Label
        $titleLabel.Text = $Title
        $titleLabel.Font = New-Object System.Drawing.Font('Segoe UI',8.3,[System.Drawing.FontStyle]::Bold)
        $titleLabel.Location = [System.Drawing.Point]::new(14,5)
        $titleLabel.AutoSize = $true
        $row.Controls.Add($titleLabel)

        $descriptionLabel = New-Object System.Windows.Forms.Label
        $descriptionLabel.Text = $Description
        $descriptionLabel.Font = New-Object System.Drawing.Font('Segoe UI',8.2)
        $descriptionLabel.ForeColor = [System.Drawing.Color]::FromArgb(108,108,108)
        $descriptionLabel.Location = [System.Drawing.Point]::new(14,23)
        $descriptionLabel.AutoSize = $true
        $row.Controls.Add($descriptionLabel)

        $buttonList = New-Object System.Collections.Generic.List[object]
        $count = $Buttons.Count
        $buttonWidth = if ($count -eq 1) { 118 } elseif ($count -eq 2) { 112 } else { 88 }
        $gap = 7
        $right = 884

        for ($i = $count - 1; $i -ge 0; $i--) {
            $button = New-Object System.Windows.Forms.Button
            $button.Text = $Buttons[$i]
            $button.Font = New-Object System.Drawing.Font('Segoe UI',8.6)
            $button.Size = [System.Drawing.Size]::new($buttonWidth,28)
            $right -= $buttonWidth
            $button.Location = [System.Drawing.Point]::new($right,9)
            $button.FlatStyle = [System.Windows.Forms.FlatStyle]::System
            $button.UseVisualStyleBackColor = $true
            $row.Controls.Add($button)
            $buttonList.Insert(0,$button)
            $right -= $gap
        }

        $separator = New-Object System.Windows.Forms.Panel
        $separator.BackColor = [System.Drawing.Color]::FromArgb(232,232,232)
        $separator.Location = [System.Drawing.Point]::new(14,45)
        $separator.Size = [System.Drawing.Size]::new(860,1)
        $row.Controls.Add($separator)

        [pscustomobject]@{
            Panel = $row
            Buttons = $buttonList.ToArray()
        }
    }

    $tenantChoiceLookup = @{}
    $tenantChoices = New-Object System.Collections.Generic.List[string]
    $autoLabel = if ($outerBoundParameters.ContainsKey('TenantId')) { 'Default tenant parameter' } else { 'Automatic / local context' }
    $tenantChoiceLookup[$autoLabel] = $null
    $tenantChoices.Add($autoLabel)

    if ($Tenants) {
        foreach ($name in @($Tenants.Keys | Sort-Object)) {
            $id = [string]$Tenants[$name]
            if ([string]::IsNullOrWhiteSpace($id)) { continue }
            $label = [string]$name
            if ($tenantChoiceLookup.ContainsKey($label)) {
                $label = "$label ($id)"
            }
            $tenantChoiceLookup[$label] = $id.Trim()
            $tenantChoices.Add($label)
        }
    }

    function Get-SelectedTenantId {
        if ($tenantSelector.SelectedItem) {
            $selected = [string]$tenantSelector.SelectedItem
            if ($tenantChoiceLookup.ContainsKey($selected) -and $tenantChoiceLookup[$selected]) {
                return [string]$tenantChoiceLookup[$selected]
            }
        }

        if ($outerBoundParameters.ContainsKey('TenantId')) {
            return [string]$TenantId
        }

        $null
    }

    function Get-GuiAuthParameters {
        $parameters = @{
            Method = $Method
            Environment = $Environment
            ClientTimeout = $ClientTimeout
        }

        foreach ($name in @(
            'ClientId','AccessToken','Certificate','CertificateThumbprint',
            'CertificateSubjectName','SendCertificateChain','ClientSecret'
        )) {
            if ($outerBoundParameters.ContainsKey($name)) {
                $parameters[$name] = $outerBoundParameters[$name]
            }
        }

        $selectedTenant = Get-SelectedTenantId
        if ($selectedTenant) {
            $parameters.TenantId = $selectedTenant
        }

        $parameters
    }

    $ui = @{}
    $script:WdlGuiBusy = $false
    $script:WdlGuiLocalAssociation = $null
    $script:WdlGuiCloudStatus = $null

    $deviceCard = New-Card -Title 'Device' -X 14 -Y 12 -Width 508 -Height 126
    $associationCard = New-Card -Title 'Association' -X 536 -Y 12 -Width 508 -Height 126

    $ui.DeviceName = New-ValuePair -Parent $deviceCard -Caption 'Device' -Y 34
    $ui.Serial     = New-ValuePair -Parent $deviceCard -Caption 'Serial number' -Y 56
    $ui.Environment = New-ValuePair -Parent $deviceCard -Caption 'Environment' -Y 78
    $ui.Auth       = New-ValuePair -Parent $deviceCard -Caption 'Authentication' -Y 100

    $ui.Firmware   = New-ValuePair -Parent $associationCard -Caption 'Firmware' -Y 34
    $ui.LocalState = New-ValuePair -Parent $associationCard -Caption 'Local state' -Y 56
    $ui.TenantId   = New-ValuePair -Parent $associationCard -Caption 'Tenant ID' -Y 78
    $ui.Source     = New-ValuePair -Parent $associationCard -Caption 'Source' -Y 100

    $cloudCard = New-Card -Title 'Cloud association' -X 14 -Y 150 -Width 1030 -Height 84
    $ui.CloudState  = New-ValuePair -Parent $cloudCard -Caption 'State' -Y 34 -CaptionWidth 80
    $ui.CloudTenant = New-ValuePair -Parent $cloudCard -Caption 'Tenant ID' -Y 56 -CaptionWidth 80

    $cloudIdCaption = New-Object System.Windows.Forms.Label
    $cloudIdCaption.Text = 'Association ID'
    $cloudIdCaption.Font = New-Object System.Drawing.Font('Segoe UI',8.5)
    $cloudIdCaption.ForeColor = [System.Drawing.Color]::FromArgb(102,102,102)
    $cloudIdCaption.Location = [System.Drawing.Point]::new(500,34)
    $cloudIdCaption.Size = [System.Drawing.Size]::new(96,20)
    $cloudCard.Controls.Add($cloudIdCaption)

    $ui.CloudId = New-Object System.Windows.Forms.Label
    $ui.CloudId.Text = 'Unavailable'
    $ui.CloudId.Font = New-Object System.Drawing.Font('Segoe UI',8.3,[System.Drawing.FontStyle]::Bold)
    $ui.CloudId.Location = [System.Drawing.Point]::new(600,33)
    $ui.CloudId.Size = [System.Drawing.Size]::new(380,22)
    $ui.CloudId.AutoEllipsis = $true
    $cloudCard.Controls.Add($ui.CloudId)

    $tenantCaption = New-Object System.Windows.Forms.Label
    $tenantCaption.Text = 'Tenant'
    $tenantCaption.Font = New-Object System.Drawing.Font('Segoe UI',8.5)
    $tenantCaption.ForeColor = [System.Drawing.Color]::FromArgb(102,102,102)
    $tenantCaption.Location = [System.Drawing.Point]::new(500,56)
    $tenantCaption.Size = [System.Drawing.Size]::new(96,18)
    $cloudCard.Controls.Add($tenantCaption)

    $tenantSelector = New-Object System.Windows.Forms.ComboBox
    $tenantSelector.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList
    $tenantSelector.Font = New-Object System.Drawing.Font('Segoe UI',8.5)
    $tenantSelector.Location = [System.Drawing.Point]::new(600,53)
    $tenantSelector.Size = [System.Drawing.Size]::new(380,24)
    foreach ($choice in $tenantChoices) {
        [void]$tenantSelector.Items.Add($choice)
    }
    $tenantSelector.SelectedIndex = 0
    $cloudCard.Controls.Add($tenantSelector)

    $actionsTitle = New-Object System.Windows.Forms.Label
    $actionsTitle.Text = 'Actions'
    $actionsTitle.Font = New-Object System.Drawing.Font('Segoe UI',11.5,[System.Drawing.FontStyle]::Bold)
    $actionsTitle.Location = [System.Drawing.Point]::new(16,246)
    $actionsTitle.AutoSize = $true
    $content.Controls.Add($actionsTitle)

    $actionsPanel = New-Card -Title '' -X 14 -Y 272 -Width 1030 -Height 232
    $rowRefresh = New-ActionRow -Parent $actionsPanel -Title 'Refresh' -Description 'Refresh local DeviceLink and firmware information.' -Y 0 -Buttons @('Refresh')
    $rowOnline  = New-ActionRow -Parent $actionsPanel -Title 'Check online' -Description 'Query the tenant-side Device Association using the selected tenant context.' -Y 46 -Buttons @('Check online')
    $rowExport  = New-ActionRow -Parent $actionsPanel -Title 'Export DeviceLink CSV' -Description 'Export the Microsoft-generated .devicelink.csv.' -Y 92 -Buttons @('Export CSV')
    $rowOnboard = New-ActionRow -Parent $actionsPanel -Title 'Onboarding' -Description 'Create only the pre-association, or perform the complete onboarding flow.' -Y 138 -Buttons @('Pre-associate','Full associate')
    $rowOffboard = New-ActionRow -Parent $actionsPanel -Title 'Offboarding' -Description 'Remove cloud state, local state, or both.' -Y 184 -Buttons @('Cloud','Local','Full')

    $offboardSeparator = @(
        $rowOffboard.Panel.Controls |
            Where-Object { $_ -is [System.Windows.Forms.Panel] -and $_.Height -eq 1 }
    ) | Select-Object -First 1
    if ($offboardSeparator) {
        $rowOffboard.Panel.Controls.Remove($offboardSeparator)
        $offboardSeparator.Dispose()
    }

    function Get-ActionButtonByText {
        param(
            [Parameter(Mandatory)][System.Windows.Forms.Control]$Row,
            [Parameter(Mandatory)][string]$Text
        )

        $button = @(
            $Row.Controls |
                Where-Object { $_ -is [System.Windows.Forms.Button] -and $_.Text -eq $Text }
        ) | Select-Object -First 1

        if (-not $button) {
            throw "GUI action button '$Text' could not be resolved."
        }

        $button
    }

    $btnRefresh = Get-ActionButtonByText -Row $rowRefresh.Panel -Text 'Refresh'
    $btnOnline = Get-ActionButtonByText -Row $rowOnline.Panel -Text 'Check online'
    $btnExport = Get-ActionButtonByText -Row $rowExport.Panel -Text 'Export CSV'
    $btnPreassociate = Get-ActionButtonByText -Row $rowOnboard.Panel -Text 'Pre-associate'
    $btnFullAssociate = Get-ActionButtonByText -Row $rowOnboard.Panel -Text 'Full associate'
    $btnCloudOffboard = Get-ActionButtonByText -Row $rowOffboard.Panel -Text 'Cloud'
    $btnLocalOffboard = Get-ActionButtonByText -Row $rowOffboard.Panel -Text 'Local'
    $btnFullOffboard = Get-ActionButtonByText -Row $rowOffboard.Panel -Text 'Full'

    $allActionButtons = @(
        $btnRefresh,
        $btnOnline,
        $btnExport,
        $btnPreassociate,
        $btnFullAssociate,
        $btnCloudOffboard,
        $btnLocalOffboard,
        $btnFullOffboard
    )

    $activityTitle = New-Object System.Windows.Forms.Label
    $activityTitle.Text = 'Activity'
    $activityTitle.Font = New-Object System.Drawing.Font('Segoe UI',11,[System.Drawing.FontStyle]::Bold)
    $activityTitle.Location = [System.Drawing.Point]::new(16,518)
    $activityTitle.AutoSize = $true
    $content.Controls.Add($activityTitle)

    $btnClearActivity = New-Object System.Windows.Forms.Button
    $btnClearActivity.Text = 'Clear'
    $btnClearActivity.Font = New-Object System.Drawing.Font('Segoe UI',8.3)
    $btnClearActivity.Size = [System.Drawing.Size]::new(64,24)
    $btnClearActivity.FlatStyle = [System.Windows.Forms.FlatStyle]::System
    $content.Controls.Add($btnClearActivity)

    $activityCard = New-Card -Title '' -X 14 -Y 544 -Width 1030 -Height 118

    $consoleBox = New-Object System.Windows.Forms.TextBox
    $consoleBox.Location = [System.Drawing.Point]::new(12,10)
    $consoleBox.Size = [System.Drawing.Size]::new(1006,96)
    $consoleBox.Multiline = $true
    $consoleBox.ReadOnly = $true
    $consoleBox.ScrollBars = [System.Windows.Forms.ScrollBars]::Vertical
    $consoleBox.WordWrap = $false
    $consoleBox.Font = New-Object System.Drawing.Font('Consolas',8)
    $consoleBox.BackColor = [System.Drawing.Color]::FromArgb(250,250,250)
    $consoleBox.BorderStyle = [System.Windows.Forms.BorderStyle]::None
    $activityCard.Controls.Add($consoleBox)

    $statusStrip = New-Object System.Windows.Forms.StatusStrip
    $statusStrip.Dock = [System.Windows.Forms.DockStyle]::Bottom

    $statusLabel = New-Object System.Windows.Forms.ToolStripStatusLabel
    $statusLabel.Text = 'Ready'
    $statusLabel.Spring = $true
    $statusLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft

    $statusProgress = New-Object System.Windows.Forms.ToolStripProgressBar
    $statusProgress.Style = [System.Windows.Forms.ProgressBarStyle]::Marquee
    $statusProgress.MarqueeAnimationSpeed = 30
    $statusProgress.Size = [System.Drawing.Size]::new(140,16)
    $statusProgress.Visible = $false

    [void]$statusStrip.Items.Add($statusLabel)
    [void]$statusStrip.Items.Add($statusProgress)
    $form.Controls.Add($statusStrip)

    function Set-GuiStatus {
        param([string]$Text)
        $statusLabel.Text = $Text
        [System.Windows.Forms.Application]::DoEvents()
    }

    function Write-GuiConsole {
        param(
            [AllowNull()][AllowEmptyString()][string]$Message,
            [switch]$Command,
            [switch]$ErrorMessage
        )

        if ([string]::IsNullOrWhiteSpace($Message)) { return }

        $timestamp = (Get-Date).ToString('HH:mm:ss')
        $prefix = if ($Command) { '>' } elseif ($ErrorMessage) { '!' } else { '-' }
        $line = "[$timestamp] $prefix $Message"

        $consoleBox.AppendText($line + [Environment]::NewLine)
        $consoleBox.SelectionStart = $consoleBox.TextLength
        $consoleBox.ScrollToCaret()
        [System.Windows.Forms.Application]::DoEvents()
        Write-Host $line
    }

    function Write-GuiObject {
        param($InputObject)

        if ($null -eq $InputObject) { return }

        foreach ($item in @($InputObject)) {
            if ($null -eq $item) { continue }

            $properties = @($item.PSObject.Properties)
            if ($properties.Count -eq 0) {
                $text = [string]$item
                if (-not [string]::IsNullOrWhiteSpace($text)) {
                    Write-GuiConsole -Message $text
                }
                continue
            }

            $rows = New-Object System.Collections.Generic.List[object]
            foreach ($property in $properties) {
                $value = $property.Value
                if ($null -eq $value) { continue }

                if ($value -is [string] -and [string]::IsNullOrWhiteSpace($value)) { continue }

                if ($value -is [datetime]) {
                    $displayValue = $value.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
                }
                elseif ($value -is [datetimeoffset]) {
                    $displayValue = $value.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
                }
                elseif ($value -is [System.Collections.IEnumerable] -and -not ($value -is [string])) {
                    $items = @($value)
                    if ($items.Count -eq 0) { continue }
                    $displayValue = ($items | ForEach-Object { [string]$_ }) -join ', '
                }
                else {
                    $displayValue = [string]$value
                }

                if ([string]::IsNullOrWhiteSpace($displayValue)) { continue }

                $rows.Add([pscustomobject]@{
                    Name = [string]$property.Name
                    Value = $displayValue
                })
            }

            if ($rows.Count -eq 0) { continue }

            $nameWidth = [Math]::Min(
                34,
                [Math]::Max(
                    8,
                    (@($rows | ForEach-Object { $_.Name.Length }) | Measure-Object -Maximum).Maximum
                )
            )

            foreach ($row in $rows) {
                $name = $row.Name
                if ($name.Length -gt $nameWidth) {
                    $name = $name.Substring(0,$nameWidth)
                }
                Write-GuiConsole -Message (("{0,-$nameWidth} : {1}" -f $name,$row.Value))
            }
        }
    }

    function Show-GuiError {
        param([string]$Message)

        Write-GuiConsole -Message $Message -ErrorMessage
        [void][System.Windows.Forms.MessageBox]::Show(
            $form,$Message,'WindowsDeviceLink',
            [System.Windows.Forms.MessageBoxButtons]::OK,
            [System.Windows.Forms.MessageBoxIcon]::Error
        )
    }

    function Confirm-GuiAction {
        param(
            [string]$Title,
            [string]$Message
        )

        $result = [System.Windows.Forms.MessageBox]::Show(
            $form,
            $Message,
            $Title,
            [System.Windows.Forms.MessageBoxButtons]::YesNo,
            [System.Windows.Forms.MessageBoxIcon]::Warning
        )

        return ($result -eq [System.Windows.Forms.DialogResult]::Yes)
    }

    function Set-GuiBusy {
        param(
            [Parameter(Mandatory)][bool]$Busy,
            [string]$StatusText
        )

        $script:WdlGuiBusy = $Busy
        $enabled = -not $Busy

        foreach ($button in $allActionButtons) {
            $button.Enabled = $enabled
        }
        $btnClearActivity.Enabled = $enabled

        # Also disable the complete action surface as a hard interaction guard.
        # The individual button state above ensures every button paints as disabled.
        $actionsPanel.Enabled = $enabled
        $tenantSelector.Enabled = $enabled

        $statusProgress.Visible = $Busy
        $form.UseWaitCursor = $Busy

        if (-not [string]::IsNullOrWhiteSpace($StatusText)) {
            Set-GuiStatus $StatusText
        }

        foreach ($button in $allActionButtons) {
            $button.Invalidate()
            $button.Update()
        }

        $actionsPanel.Invalidate($true)
        $actionsPanel.Update()
        $tenantSelector.Invalidate()
        $tenantSelector.Update()
        [System.Windows.Forms.Application]::DoEvents()
    }

    function Refresh-LocalView {
        Set-GuiStatus 'Refreshing local state...'
        Write-GuiConsole -Message 'Refresh local state' -Command

        $bios = Get-CimInstance -ClassName Win32_BIOS -ErrorAction Stop
        $cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop
        $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue
        $support = Test-WindowsDeviceLinkSupport
        $local = Get-WindowsDeviceLinkLocalAssociation

        $script:WdlGuiLocalAssociation = $local

        $ui.DeviceName.Text = "$($cs.Manufacturer) $($cs.Model)".Trim()
        $ui.Serial.Text = [string]$bios.SerialNumber

        if ($support.Environment -eq 'WindowsPE') {
            $environmentText = 'Windows PE'
        }
        elseif ($os -and [string]$os.Caption -match 'Windows 11') {
            $environmentText = 'Windows 11'
        }
        elseif ($os -and -not [string]::IsNullOrWhiteSpace([string]$os.Caption)) {
            $environmentText = ([string]$os.Caption).Replace('Microsoft ','')
        }
        else {
            $environmentText = [string]$support.Environment
        }

        if (-not $support.Supported) {
            $environmentText += " (unsupported: $($support.Reason))"
        }

        $ui.Environment.Text = $environmentText

        $selectedTenant = Get-SelectedTenantId
        $ui.Auth.Text = if ($selectedTenant) { "$Method | $selectedTenant" } else { $Method }

        $ui.Firmware.Text = [string]$local.FirmwareState

        $friendlyLocalState = switch ([string]$local.LocalAssociationState) {
            'CompleteAssociationFirmwareState' { 'Complete association' }
            'BaseIdentity' { 'Base identity' }
            'NoFirmwareState' { 'No firmware state' }
            'IncompleteFirmwareState' { 'Incomplete firmware state' }
            default { [string]$local.LocalAssociationState }
        }
        $ui.LocalState.Text = $friendlyLocalState

        $ui.TenantId.Text = if ($local.TenantId) { [string]$local.TenantId } else { 'Unavailable' }

        $technicalSource = if ([string]$local.TrustLevel -eq 'Unavailable' -and [string]$local.Source -eq 'Unavailable') {
            'Unavailable'
        }
        elseif ([string]::IsNullOrWhiteSpace([string]$local.TrustLevel)) {
            [string]$local.Source
        }
        elseif ([string]::IsNullOrWhiteSpace([string]$local.Source)) {
            [string]$local.TrustLevel
        }
        else {
            "$($local.TrustLevel) | $($local.Source)"
        }

        $friendlySource = switch ([string]$local.TrustLevel) {
            'CorrelatedLocalSources' { 'Correlated local sources' }
            'LocalRegistryHint' { 'Local registry hint' }
            'StructurallyObservedJwtClaim' { 'Association JWT claim' }
            'Conflict' { 'Conflict' }
            'Unavailable' { 'Unavailable' }
            default { $technicalSource }
        }

        $ui.Source.Text = $friendlySource

        $toolTip.SetToolTip($ui.TenantId, [string]$ui.TenantId.Text)
        $toolTip.SetToolTip($ui.Source, $technicalSource)

        Set-GuiStatus "Local state refreshed | $($local.FirmwareState)"
        Write-GuiConsole -Message "Local state: $($local.FirmwareState), tenant source: $($local.Source)"
    }

    function Refresh-CloudView {
        param(
            [switch]$WriteCommand
        )

        $parameters = Get-GuiAuthParameters
        $parameters.Online = $true

        if ($WriteCommand) {
            Write-GuiConsole -Message "Get-WindowsDeviceLinkStatus -Online -Method $Method" -Command
        }

        $cloud = Get-WindowsDeviceLinkStatus @parameters
        $script:WdlGuiCloudStatus = $cloud

        $ui.CloudState.Text = [string]$cloud.AssociationState
        $ui.CloudTenant.Text = if ($cloud.TenantId) { [string]$cloud.TenantId } else { 'Unavailable' }
        $ui.CloudId.Text = if ($cloud.AssociationId) { [string]$cloud.AssociationId } else { 'Unavailable' }

        $toolTip.SetToolTip($ui.CloudTenant, [string]$ui.CloudTenant.Text)
        $toolTip.SetToolTip($ui.CloudId, [string]$ui.CloudId.Text)

        return $cloud
    }

    function Invoke-GuiRefresh {
        if ($script:WdlGuiBusy) { return }

        Set-GuiBusy -Busy $true -StatusText 'Refreshing local state...'
        try { Refresh-LocalView }
        catch { Show-GuiError $_.Exception.Message }
        finally { Set-GuiBusy -Busy $false }
    }

    function Invoke-GuiOnline {
        if ($script:WdlGuiBusy) { return }

        Set-GuiBusy -Busy $true -StatusText 'Checking tenant-side Device Association...'
        try {
            $cloud = Refresh-CloudView -WriteCommand
            Write-GuiObject $cloud

            Set-GuiStatus 'Cloud lookup completed'
        }
        catch {
            Set-GuiStatus 'Cloud lookup failed'
            Show-GuiError $_.Exception.Message
        }
        finally { Set-GuiBusy -Busy $false }
    }

    function Invoke-GuiExport {
        if ($script:WdlGuiBusy) { return }

        Set-GuiBusy -Busy $true -StatusText 'Choose export folder...'

        $dialog = New-Object System.Windows.Forms.FolderBrowserDialog
        $dialog.Description = 'Choose a folder for the DeviceLink CSV'

        try {
            if ($dialog.ShowDialog($form) -ne [System.Windows.Forms.DialogResult]::OK) {
                Set-GuiStatus 'Export cancelled'
                return
            }

            Set-GuiStatus 'Exporting DeviceLink CSV...'
            Write-GuiConsole -Message "Get-WindowsDeviceLink -OutputDirectory '$($dialog.SelectedPath)'" -Command

            $file = Get-WindowsDeviceLink -OutputDirectory $dialog.SelectedPath
            Write-GuiObject $file

            [void][System.Windows.Forms.MessageBox]::Show(
                $form,
                ('Exported to:' + [Environment]::NewLine + $file.FullName),
                'WindowsDeviceLink',
                [System.Windows.Forms.MessageBoxButtons]::OK,
                [System.Windows.Forms.MessageBoxIcon]::Information
            )

            Refresh-LocalView
        }
        catch { Show-GuiError $_.Exception.Message }
        finally {
            Set-GuiBusy -Busy $false
            $dialog.Dispose()
        }
    }

    function Invoke-GuiPreassociate {
        if ($script:WdlGuiBusy) { return }

        if (-not (Confirm-GuiAction -Title 'Create pre-association' -Message 'Create or attempt the tenant-side Device Association pre-association for this device?')) {
            return
        }

        Set-GuiBusy -Busy $true -StatusText 'Creating pre-association...'
        try {
            Write-GuiConsole -Message 'Get-WindowsDeviceLink' -Command
            $identity = Get-WindowsDeviceLink

            $parameters = Get-GuiAuthParameters
            $parameters.InputObject = $identity
            $parameters.Confirm = $false

            Write-GuiConsole -Message "Get-WindowsDeviceLink | Register-WindowsDeviceLink -Method $Method" -Command
            $result = Register-WindowsDeviceLink @parameters
            Write-GuiObject $result

            $script:WdlGuiCloudStatus = $null
            Refresh-LocalView
            $verifiedCloud = Refresh-CloudView -WriteCommand
            Write-GuiObject $verifiedCloud
            Set-GuiStatus 'Pre-association completed'
        }
        catch {
            Set-GuiStatus 'Pre-association failed'
            Show-GuiError $_.Exception.Message
        }
        finally { Set-GuiBusy -Busy $false }
    }

    function Invoke-GuiFullAssociate {
        if ($script:WdlGuiBusy) { return }

        if (-not (Confirm-GuiAction -Title 'Full association' -Message 'Ensure pre-association exists and complete Device Association on this device?')) {
            return
        }

        Set-GuiBusy -Busy $true -StatusText 'Completing onboarding...'
        try {
            $parameters = Get-GuiAuthParameters
            $parameters.CompleteAssociation = $true
            $parameters.Confirm = $false

            Write-GuiConsole -Message "Initialize-WindowsDeviceLink -Method $Method -CompleteAssociation" -Command

            $resultObjects = New-Object System.Collections.Generic.List[object]
            & {
                Initialize-WindowsDeviceLink @parameters
            } 6>&1 | ForEach-Object {
                if ($_ -is [System.Management.Automation.InformationRecord]) {
                    $message = [string]$_.MessageData
                    if ($message) { Write-GuiConsole -Message $message }
                }
                else {
                    $resultObjects.Add($_)
                }
                [System.Windows.Forms.Application]::DoEvents()
            }

            $result = @($resultObjects.ToArray()) | Select-Object -Last 1
            Write-GuiObject $result

            $script:WdlGuiCloudStatus = $null
            Refresh-LocalView
            $verifiedCloud = Refresh-CloudView -WriteCommand
            Write-GuiObject $verifiedCloud
            Set-GuiStatus 'Full association completed'
        }
        catch {
            Set-GuiStatus 'Full association failed'
            Show-GuiError $_.Exception.Message
        }
        finally { Set-GuiBusy -Busy $false }
    }

    function Invoke-GuiCloudOffboard {
        if ($script:WdlGuiBusy) { return }

        if (-not (Confirm-GuiAction -Title 'Cloud offboarding' -Message 'Remove only the tenant-side Device Association record? Local DeviceLink firmware will remain unchanged.')) {
            return
        }

        Set-GuiBusy -Busy $true -StatusText 'Removing cloud association...'
        try {
            $parameters = Get-GuiAuthParameters
            $parameters.Confirm = $false

            Write-GuiConsole -Message "Remove-WindowsDeviceLinkAssociation -Method $Method" -Command
            $result = Remove-WindowsDeviceLinkAssociation @parameters
            Write-GuiObject $result

            $script:WdlGuiCloudStatus = $null
            $ui.CloudState.Text = 'Not checked'
            $ui.CloudTenant.Text = 'Unavailable'
            $ui.CloudId.Text = 'Unavailable'

            Refresh-LocalView
            Set-GuiStatus 'Cloud offboarding completed'
        }
        catch {
            Set-GuiStatus 'Cloud offboarding failed'
            Show-GuiError $_.Exception.Message
        }
        finally { Set-GuiBusy -Busy $false }
    }

    function Invoke-GuiLocalOffboard {
        if ($script:WdlGuiBusy) { return }

        if (-not (Confirm-GuiAction -Title 'Local offboarding' -Message 'Reset all known local DeviceLink UEFI variables? The tenant-side Device Association record will remain unchanged.')) {
            return
        }

        Set-GuiBusy -Busy $true -StatusText 'Resetting local DeviceLink state...'
        try {
            Write-GuiConsole -Message 'Reset-WindowsDeviceLinkFirmwareState' -Command
            $result = Reset-WindowsDeviceLinkFirmwareState -Confirm:$false
            Write-GuiObject $result

            $script:WdlGuiCloudStatus = $null
            Refresh-LocalView
            Set-GuiStatus 'Local offboarding completed'
        }
        catch {
            Set-GuiStatus 'Local offboarding failed'
            Show-GuiError $_.Exception.Message
        }
        finally { Set-GuiBusy -Busy $false }
    }

    function Invoke-GuiFullOffboard {
        if ($script:WdlGuiBusy) { return }

        $message = @(
            'This performs full DeviceLink offboarding:'
            ''
            '1. Verify tenant-side Device Association state'
            '2. Remove the cloud Device Association if present'
            '3. Reset all known local DeviceLink UEFI variables'
            ''
            'This does not remove the Entra device or MDM enrollment record.'
            ''
            'Continue?'
        ) -join [Environment]::NewLine

        if (-not (Confirm-GuiAction -Title 'Full DeviceLink offboarding' -Message $message)) {
            return
        }

        Set-GuiBusy -Busy $true -StatusText 'Performing full offboarding...'
        try {
            $auth = Get-GuiAuthParameters
            $statusParameters = @{}
            foreach ($key in $auth.Keys) { $statusParameters[$key] = $auth[$key] }
            $statusParameters.Online = $true

            Write-GuiConsole -Message "Get-WindowsDeviceLinkStatus -Online -Method $Method" -Command
            $cloud = Get-WindowsDeviceLinkStatus @statusParameters
            Write-GuiObject $cloud

            if ($null -eq $cloud.AssociationPresent -or [string]$cloud.AssociationState -eq 'Unknown') {
                throw 'Cloud Device Association state is indeterminate. Local firmware was not reset.'
            }

            if ($cloud.AssociationPresent) {
                $removeParameters = @{}
                foreach ($key in $auth.Keys) { $removeParameters[$key] = $auth[$key] }
                $removeParameters.Confirm = $false

                Write-GuiConsole -Message "Remove-WindowsDeviceLinkAssociation -Method $Method" -Command
                $removed = Remove-WindowsDeviceLinkAssociation @removeParameters
                Write-GuiObject $removed
            }
            else {
                Write-GuiConsole -Message 'No tenant-side Device Association exists. Cloud removal skipped.'
            }

            Write-GuiConsole -Message 'Reset-WindowsDeviceLinkFirmwareState' -Command
            $reset = Reset-WindowsDeviceLinkFirmwareState -Confirm:$false
            Write-GuiObject $reset

            $script:WdlGuiCloudStatus = $null
            $ui.CloudState.Text = 'Not checked'
            $ui.CloudTenant.Text = 'Unavailable'
            $ui.CloudId.Text = 'Unavailable'

            Refresh-LocalView
            Set-GuiStatus 'Full offboarding completed'
        }
        catch {
            Set-GuiStatus 'Full offboarding failed'
            Show-GuiError $_.Exception.Message
        }
        finally { Set-GuiBusy -Busy $false }
    }

    $btnRefresh.Add_Click({ Invoke-GuiRefresh })
    $btnOnline.Add_Click({ Invoke-GuiOnline })
    $btnExport.Add_Click({ Invoke-GuiExport })
    $btnPreassociate.Add_Click({ Invoke-GuiPreassociate })
    $btnFullAssociate.Add_Click({ Invoke-GuiFullAssociate })
    $btnCloudOffboard.Add_Click({ Invoke-GuiCloudOffboard })
    $btnLocalOffboard.Add_Click({ Invoke-GuiLocalOffboard })
    $btnFullOffboard.Add_Click({ Invoke-GuiFullOffboard })

    $tenantSelector.Add_SelectedIndexChanged({
        if (-not $script:WdlGuiBusy) {
            $selectedTenant = Get-SelectedTenantId
            $ui.Auth.Text = if ($selectedTenant) { "$Method | $selectedTenant" } else { $Method }
        }
    })

    $btnClearActivity.Add_Click({
        $consoleBox.Clear()
        Write-GuiConsole -Message 'Activity log cleared.'
    })

    $form.Add_FormClosing({
        param($sender,$eventArgs)

        if ($script:WdlGuiBusy) {
            $eventArgs.Cancel = $true
            Write-GuiConsole -Message 'Close request ignored because an operation is still running.'
        }
    })

    $form.Add_Shown({
        Write-GuiConsole -Message "WindowsDeviceLink dashboard opened. Authentication method: $Method."
        Set-GuiBusy -Busy $true -StatusText 'Loading local state...'
        try { Refresh-LocalView }
        catch { Show-GuiError $_.Exception.Message }
        finally { Set-GuiBusy -Busy $false }
    })

    function Resize-GuiLayout {
        $fullWidth = [Math]::Max(760,$content.ClientSize.Width - 32)
        $gap = 14
        $halfWidth = [Math]::Floor(($fullWidth - $gap) / 2)

        if ($fullWidth -ge 900) {
            $deviceCard.Location = [System.Drawing.Point]::new(14,12)
            $deviceCard.Size = [System.Drawing.Size]::new([int]$halfWidth,126)
            $associationCard.Location = [System.Drawing.Point]::new((14 + $halfWidth + $gap),12)
            $associationCard.Size = [System.Drawing.Size]::new([int]$halfWidth,126)
            $cloudY = 150
        }
        else {
            $deviceCard.Location = [System.Drawing.Point]::new(14,12)
            $deviceCard.Size = [System.Drawing.Size]::new([int]$fullWidth,126)
            $associationCard.Location = [System.Drawing.Point]::new(14,150)
            $associationCard.Size = [System.Drawing.Size]::new([int]$fullWidth,126)
            $cloudY = 288
        }

        $cloudCard.Location = [System.Drawing.Point]::new(14,$cloudY)
        $cloudCard.Width = $fullWidth

        $actionsY = $cloudY + 96
        $actionsTitle.Location = [System.Drawing.Point]::new(16,$actionsY)
        $actionsPanel.Location = [System.Drawing.Point]::new(14,($actionsY + 26))
        $actionsPanel.Width = $fullWidth

        $activityY = $actionsY + 270
        $activityTitle.Location = [System.Drawing.Point]::new(16,$activityY)

        # Align Clear to the same right edge used by the action buttons.
        # Action buttons sit 16 px inside the right edge of the Actions panel.
        $clearX = $actionsPanel.Right - 16 - $btnClearActivity.Width
        $clearY = $activityY - 5
        $btnClearActivity.Location = [System.Drawing.Point]::new($clearX,$clearY)

        $activityCard.Location = [System.Drawing.Point]::new(14,($activityY + 26))
        $activityCard.Width = $fullWidth
        $consoleBox.Width = $fullWidth - 24


        foreach ($row in @($rowRefresh,$rowOnline,$rowExport,$rowOnboard,$rowOffboard)) {
            $row.Panel.Width = $fullWidth

            $buttons = @($row.Buttons)
            $right = $fullWidth - 16
            for ($i = $buttons.Count - 1; $i -ge 0; $i--) {
                $right -= $buttons[$i].Width
                $buttons[$i].Left = $right
                $right -= 7
            }

            $row.Panel.Controls |
                Where-Object { $_ -is [System.Windows.Forms.Panel] -and $_.Height -eq 1 } |
                ForEach-Object { $_.Width = [Math]::Max(480,$fullWidth - 28) }
        }

        $requiredHeight = $activityCard.Bottom + 2
        $availableHeight = [Math]::Max(0,$content.ClientSize.Height - 2)
        $scrollTolerance = 10

        if ($requiredHeight -gt ($availableHeight + $scrollTolerance)) {
            $content.AutoScroll = $true
            $content.HorizontalScroll.Enabled = $false
            $content.HorizontalScroll.Visible = $false
            $content.AutoScrollMinSize = [System.Drawing.Size]::new(1,$requiredHeight)
        }
        else {
            $content.AutoScrollMinSize = [System.Drawing.Size]::Empty
            $content.AutoScroll = $false
            $content.VerticalScroll.Value = 0
        }
    }

    $form.Add_Resize({ Resize-GuiLayout })

    try {
        Resize-GuiLayout
        [void]$form.ShowDialog()
    }
    finally {
        $toolTip.Dispose()
        $form.Dispose()
        Remove-Variable WdlGuiLocalAssociation -Scope Script -ErrorAction SilentlyContinue
        Remove-Variable WdlGuiCloudStatus -Scope Script -ErrorAction SilentlyContinue
        Remove-Variable WdlGuiBusy -Scope Script -ErrorAction SilentlyContinue
    }
}