Tools/NITMSIXEventlogTracer.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    NITMSIXEventlogTracer - WPF GUI for recording and analysing MSIX deployment events.

.DESCRIPTION
    Enables MSIX diagnostic event log channels, records events during a tracing
    session bounded by a Start/Stop action, then displays all collected events in a
    searchable, filterable WPF window with copy/save/detail capabilities.

    Run as Administrator for full diagnostic channel access (wevtutil requires elevation).
    Read-only event queries work without elevation.

.PARAMETER DebugLog
    Writes a transcript with identity, environment variables, and self-elevation
    trace to $env:APPDATA\NITTracer-debug.log (with fallback paths). Off by default.

.EXAMPLE
    . D:\...\NITMSIXEventlogTracer.ps1

.EXAMPLE
    .\NITMSIXEventlogTracer.ps1 -DebugLog

.NOTES
    Andreas Nick, 2026 - https://www.nick-it.de
    Keyboard shortcuts: F5 = Start/Stop recording | Ctrl+F = Focus search | Escape = Clear search
#>

[CmdletBinding()]
param(
    [switch] $DebugLog
)

if ($DebugLog) {
    $BeaconPaths = @(
        "$env:APPDATA\NITTracer-debug.log",
        "$env:LOCALAPPDATA\NITTracer-debug.log",
        "$env:TEMP\NITTracer-debug.log",
        "$env:USERPROFILE\Desktop\NITTracer-debug.log",
        'C:\Windows\Temp\NITTracer-debug.log'
    )
    $TranscriptLog = $null
    foreach ($p in $BeaconPaths) {
        try {
            "$(Get-Date -Format 'HH:mm:ss.fff') beacon PID=$PID path=$p" | Add-Content -LiteralPath $p -ErrorAction Stop
            if ($null -eq $TranscriptLog) { $TranscriptLog = $p }
        } catch {}
    }
    try { Start-Transcript -Path $TranscriptLog -Append -Force -ErrorAction SilentlyContinue | Out-Null } catch {}
    "$(Get-Date -Format 'HH:mm:ss.fff') === Tracer started ===" | Out-Host
    " PID : $PID"                                   | Out-Host
    " Identity : $([Security.Principal.WindowsIdentity]::GetCurrent().Name)" | Out-Host
    " IsAdmin : $(([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole('Administrator'))" | Out-Host
    " ScriptPath : $($MyInvocation.MyCommand.Path)"          | Out-Host
    " PSVersion : $($PSVersionTable.PSVersion)"             | Out-Host
    " CWD : $((Get-Location).Path)"                   | Out-Host
    ' --- env: ---'                                              | Out-Host
    Get-ChildItem env: | Sort-Object Name | ForEach-Object {
        " {0,-32} = {1}" -f $_.Name, $_.Value | Out-Host
    }
    ' --- /env ---'                                              | Out-Host
}

# Self-elevate via UAC when not running as Administrator.
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
        [Security.Principal.WindowsBuiltInRole]::Administrator)) {
    if ($DebugLog) { " -> not admin, attempting Start-Process -Verb RunAs" | Out-Host }
    $scriptPath = $MyInvocation.MyCommand.Path
    if ($scriptPath) {
        $psExe = [System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName
        $argList = "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$scriptPath`""
        if ($DebugLog) { $argList += ' -DebugLog' }
        Start-Process -FilePath $psExe -ArgumentList $argList -Verb RunAs -WindowStyle Hidden
        if ($DebugLog) { try { Stop-Transcript | Out-Null } catch {} }
        exit
    }
    # Dot-sourced without a resolvable path: continue without elevation; status bar will warn.
}

Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase

# ---------------------------------------------------------------------------
# Script-scope state
# ---------------------------------------------------------------------------
$script:StartTime   = $null
$script:EndTime     = $null
$script:AllEvents   = New-Object System.Collections.Generic.List[PSObject]
$script:IsRecording = $false
$script:ClockTimer  = $null

$script:Channels = @(
    'Microsoft-Windows-AppXDeployment/Operational',
    'Microsoft-Windows-AppXDeployment/Diagnostic',
    'Microsoft-Windows-AppXDeploymentServer/Operational',
    'Microsoft-Windows-AppXDeploymentServer/Diagnostic',
    'Microsoft-Windows-AppXDeployment-Server/Operational',
    'Microsoft-Windows-AppxPackaging/Operational',
    'Microsoft-Windows-AppxPackaging/Debug'
)

# ---------------------------------------------------------------------------
# Helper functions
# ---------------------------------------------------------------------------

function Test-IsAdmin {
    $id = [Security.Principal.WindowsIdentity]::GetCurrent()
    return (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole(
        [Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Enable-MSIXChannels {
    foreach ($ch in $script:Channels) {
        try { [void](wevtutil sl $ch /e:true /q:true /ms:52428800 2>$null) } catch { }
    }
}

function Get-MSIXEvents {
    param(
        [Parameter(Mandatory = $true)] [datetime] $Since,
        [Parameter(Mandatory = $true)] [datetime] $Until
    )
    $rows = New-Object System.Collections.Generic.List[PSObject]
    foreach ($ch in $script:Channels) {
        $short = $ch -replace 'Microsoft-Windows-', ''
        try {
            $events = @(
                Get-WinEvent -LogName $ch -MaxEvents 5000 -ErrorAction Stop |
                Where-Object { $_.TimeCreated -ge $Since -and $_.TimeCreated -le $Until }
            )
        }
        catch { continue }

        foreach ($ev in $events) {
            $msg = if ($ev.Message) { $ev.Message } else { '(no message)' }
            $firstLine = ($msg -split '\r?\n')[0]
            if ($firstLine.Length -gt 220) { $firstLine = $firstLine.Substring(0, 220) + ' [...]' }

            $row = [PSCustomObject]@{
                Time         = $ev.TimeCreated
                TimeStr      = $ev.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss.fff')
                Level        = if ($ev.LevelDisplayName) { $ev.LevelDisplayName } else { "Level $($ev.Level)" }
                LevelN       = [int]$ev.Level
                EventId      = $ev.Id
                Channel      = $ch
                ChannelShort = $short
                MessageShort = $firstLine
                FullMessage  = $msg
                Provider     = if ($ev.ProviderName)        { $ev.ProviderName }        else { '' }
                Task         = if ($ev.TaskDisplayName)     { $ev.TaskDisplayName }     else { '' }
                Keywords     = if ($ev.KeywordsDisplayNames){ $ev.KeywordsDisplayNames -join ', ' } else { '' }
                ActivityId   = if ($ev.ActivityId)          { $ev.ActivityId.ToString() } else { '' }
            }
            $null = $rows.Add($row)
        }
    }
    return @($rows | Sort-Object Time)
}

function Get-DetailText {
    param($Row)
    if ($null -eq $Row) { return 'Select an event to see details.' }
    $sb = New-Object System.Text.StringBuilder
    $null = $sb.AppendLine("Time : $($Row.TimeStr)")
    $null = $sb.AppendLine("Level : $($Row.Level) (Level code: $($Row.LevelN))")
    $null = $sb.AppendLine("Event Id : $($Row.EventId)")
    $null = $sb.AppendLine("Channel : $($Row.Channel)")
    $null = $sb.AppendLine("Provider : $($Row.Provider)")
    if ($Row.Task)       { $null = $sb.AppendLine("Task : $($Row.Task)") }
    if ($Row.Keywords)   { $null = $sb.AppendLine("Keywords : $($Row.Keywords)") }
    if ($Row.ActivityId) { $null = $sb.AppendLine("Activity Id : $($Row.ActivityId)") }
    $null = $sb.AppendLine('')
    $null = $sb.AppendLine('--- Message ---')
    $null = $sb.AppendLine($Row.FullMessage)
    return $sb.ToString()
}

function Format-EventsAsText {
    param([object[]] $Events)
    $sb = New-Object System.Text.StringBuilder
    $null = $sb.AppendLine("NIT MSIX EventlogTracer -- Export: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
    if ($script:StartTime) {
        $null = $sb.AppendLine("Recording : $($script:StartTime.ToString('yyyy-MM-dd HH:mm:ss')) - $($script:EndTime.ToString('HH:mm:ss'))")
    }
    $null = $sb.AppendLine('-' * 80)
    $null = $sb.AppendLine('')
    foreach ($ev in $Events) {
        $lvl = $ev.Level.PadRight(9)
        $null = $sb.AppendLine("$($ev.TimeStr) [$lvl] Id=$($ev.EventId) $($ev.ChannelShort)")
        $null = $sb.AppendLine($ev.FullMessage)
        $null = $sb.AppendLine('')
    }
    return $sb.ToString()
}

function Format-EventsAsCsv {
    param([object[]] $Events)
    $sb = New-Object System.Text.StringBuilder
    $null = $sb.AppendLine('Time,Level,EventId,Channel,Provider,Message')
    foreach ($ev in $Events) {
        $msg = $ev.FullMessage -replace '"', '""'
        $null = $sb.AppendLine("`"$($ev.TimeStr)`",`"$($ev.Level)`",$($ev.EventId),`"$($ev.ChannelShort)`",`"$($ev.Provider)`",`"$msg`"")
    }
    return $sb.ToString()
}

function New-WindowIcon {
    # Programmatically draw the NIT MSIX logo: blue rounded square + white hex package + magnifier
    $size   = 32
    $visual = New-Object System.Windows.Media.DrawingVisual
    $dc     = $visual.RenderOpen()

    # Background - blue rounded rectangle
    $blue  = New-Object System.Windows.Media.SolidColorBrush(
        [System.Windows.Media.Color]::FromRgb(0, 120, 212))
    $rect  = New-Object System.Windows.Rect(0, 0, $size, $size)
    $dc.DrawRoundedRectangle($blue, $null, $rect, 5, 5)

    # White outline of hex package
    $whitePen = New-Object System.Windows.Media.Pen([System.Windows.Media.Brushes]::White, 1.5)
    $pg  = New-Object System.Windows.Media.PathGeometry
    $fig = New-Object System.Windows.Media.PathFigure
    $fig.StartPoint  = New-Object System.Windows.Point(16, 4)
    $fig.IsClosed    = $true
    $pts = @(
        (New-Object System.Windows.Point(25, 9)),
        (New-Object System.Windows.Point(25, 19)),
        (New-Object System.Windows.Point(16, 24)),
        (New-Object System.Windows.Point(7,  19)),
        (New-Object System.Windows.Point(7,  9))
    )
    foreach ($pt in $pts) {
        $null = $fig.Segments.Add((New-Object System.Windows.Media.LineSegment($pt, $true)))
    }
    $null = $pg.Figures.Add($fig)
    $dc.DrawGeometry($null, $whitePen, $pg)
    # Mid-seam of box
    $dc.DrawLine($whitePen, (New-Object System.Windows.Point(7, 9)),
                            (New-Object System.Windows.Point(16, 14)))
    $dc.DrawLine($whitePen, (New-Object System.Windows.Point(25, 9)),
                            (New-Object System.Windows.Point(16, 14)))
    $dc.DrawLine($whitePen, (New-Object System.Windows.Point(16, 14)),
                            (New-Object System.Windows.Point(16, 24)))

    # Magnifier (bottom-right overlay)
    $magnPen = New-Object System.Windows.Media.Pen([System.Windows.Media.Brushes]::White, 1.5)
    $dc.DrawEllipse($null, $magnPen, (New-Object System.Windows.Point(23, 23)), 4.5, 4.5)
    $dc.DrawLine($magnPen, (New-Object System.Windows.Point(26, 26)),
                           (New-Object System.Windows.Point(30, 30)))

    $dc.Close()
    $rtb = New-Object System.Windows.Media.Imaging.RenderTargetBitmap(
        $size, $size, 96, 96, [System.Windows.Media.PixelFormats]::Pbgra32)
    $rtb.Render($visual)
    return $rtb
}

# Functions that reference bound controls are defined here; they access
# control variables through PowerShell's scope chain (late-bound at call time).
function Update-EventFilter {
    $text     = $TxtFilter.Text.Trim()
    $levelSel = $CboLevel.SelectedIndex   # 0=All 1=Error+Warning 2=Error only
    $pkgText  = $TxtPackageFilter.Text.Trim()

    $filtered = $script:AllEvents | Where-Object {
        $ev = $_
        $passLevel = switch ($levelSel) {
            1       { $ev.LevelN -le 3 }
            2       { $ev.LevelN -le 2 }
            default { $true }
        }
        $passText = $true
        if ($text -ne '') {
            $escaped  = [regex]::Escape($text)
            $passText = (
                $ev.TimeStr      -match $escaped -or
                $ev.Level        -match $escaped -or
                $ev.ChannelShort -match $escaped -or
                $ev.MessageShort -match $escaped -or
                $ev.FullMessage  -match $escaped -or
                ([string]$ev.EventId) -eq $text
            )
        }
        $passPkg = $true
        if ($pkgText -ne '') {
            $passPkg = ($ev.FullMessage -match [regex]::Escape($pkgText))
        }
        return ($passLevel -and $passText -and $passPkg)
    }

    $arr = @($filtered)
    $EventGrid.ItemsSource = $arr
    $total = $script:AllEvents.Count
    $shown = $arr.Count
    $TxtFilterCount.Text = "Showing $shown of $total"
    $TxtEventCount.Text  = "$shown / $total events"
}

function Show-Results {
    $RecordingPanel.Visibility = [System.Windows.Visibility]::Collapsed
    $EventGrid.Visibility      = [System.Windows.Visibility]::Visible
    $FilterBar.Visibility      = [System.Windows.Visibility]::Visible
    $BtnCopyAll.IsEnabled      = $true
    $BtnCopySelected.IsEnabled = $true
    $BtnSave.IsEnabled         = $true
    $BtnClear.IsEnabled        = $true
    Update-EventFilter
}

function Reset-ToReady {
    param([string] $SplashText = 'Ready to record MSIX deployment events')
    $script:AllEvents.Clear()
    $RecordingPanel.Visibility = [System.Windows.Visibility]::Visible
    $EventGrid.Visibility      = [System.Windows.Visibility]::Collapsed
    $FilterBar.Visibility      = [System.Windows.Visibility]::Collapsed
    $TxtSplashStatus.Text      = $SplashText
    $TxtSplashSub.Text         = "Press 'Start Recording', perform your MSIX operation, then press Stop."
    $RecordProgress.IsIndeterminate = $false
    $RecordProgress.Visibility = [System.Windows.Visibility]::Collapsed
    $RecordingDot.Visibility   = [System.Windows.Visibility]::Collapsed
    $BtnCopyAll.IsEnabled      = $false
    $BtnCopySelected.IsEnabled = $false
    $BtnSave.IsEnabled         = $false
    $BtnClear.IsEnabled        = $false
    $TxtDetails.Text           = 'Select an event to see details.'
    $TxtEventCount.Text        = ''
    $TxtFilterCount.Text       = ''
    $TxtFilter.Text            = ''
    $TxtRecordClock.Text       = ''
}

# ---------------------------------------------------------------------------
# XAML
# ---------------------------------------------------------------------------
[xml] $xaml = @'
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="NIT MSIX EventlogTracer"
    Height="720" Width="1120"
    MinHeight="450" MinWidth="660"
    WindowStartupLocation="CenterScreen"
    FontFamily="Segoe UI" FontSize="12"
    Background="#F3F3F3">

  <Window.Resources>

    <!-- Shared button template with hover/press/disabled states -->
    <ControlTemplate x:Key="RoundBtnTpl" TargetType="Button">
      <Border Name="Bd"
              Background="{TemplateBinding Background}"
              BorderBrush="{TemplateBinding BorderBrush}"
              BorderThickness="{TemplateBinding BorderThickness}"
              CornerRadius="3"
              Padding="{TemplateBinding Padding}">
        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
      </Border>
      <ControlTemplate.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
          <Setter TargetName="Bd" Property="Opacity" Value="0.85"/>
        </Trigger>
        <Trigger Property="IsPressed" Value="True">
          <Setter TargetName="Bd" Property="Opacity" Value="0.65"/>
        </Trigger>
        <Trigger Property="IsEnabled" Value="False">
          <Setter Property="Opacity" Value="0.35"/>
        </Trigger>
      </ControlTemplate.Triggers>
    </ControlTemplate>

    <Style x:Key="DarkBtn" TargetType="Button">
      <Setter Property="Background" Value="#2D2D3F"/>
      <Setter Property="Foreground" Value="White"/>
      <Setter Property="BorderBrush" Value="#444"/>
      <Setter Property="BorderThickness" Value="1"/>
      <Setter Property="Padding" Value="10,4"/>
      <Setter Property="Margin" Value="0,0,4,0"/>
      <Setter Property="Cursor" Value="Hand"/>
      <Setter Property="Template" Value="{StaticResource RoundBtnTpl}"/>
    </Style>

    <Style x:Key="StartBtn" TargetType="Button" BasedOn="{StaticResource DarkBtn}">
      <Setter Property="Background" Value="#0078D4"/>
      <Setter Property="BorderBrush" Value="#005A9E"/>
      <Setter Property="FontWeight" Value="SemiBold"/>
      <Setter Property="Padding" Value="16,5"/>
      <Setter Property="Margin" Value="0,0,14,0"/>
    </Style>

    <Style x:Key="SmallBtn" TargetType="Button">
      <Setter Property="Background" Value="#E1E1E1"/>
      <Setter Property="BorderBrush" Value="#BBBBBB"/>
      <Setter Property="BorderThickness" Value="1"/>
      <Setter Property="Padding" Value="8,2"/>
      <Setter Property="Margin" Value="0,0,4,0"/>
      <Setter Property="Cursor" Value="Hand"/>
      <Setter Property="FontSize" Value="11"/>
      <Setter Property="Template" Value="{StaticResource RoundBtnTpl}"/>
    </Style>

    <!-- DataGrid row colouring by event level -->
    <Style x:Key="EventRowStyle" TargetType="DataGridRow">
      <Style.Triggers>
        <DataTrigger Binding="{Binding LevelN}" Value="1">
          <Setter Property="Background" Value="#FFDDDD"/>
          <Setter Property="Foreground" Value="#8B0000"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding LevelN}" Value="2">
          <Setter Property="Background" Value="#FFDDDD"/>
          <Setter Property="Foreground" Value="#8B0000"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding LevelN}" Value="3">
          <Setter Property="Background" Value="#FFF8DC"/>
          <Setter Property="Foreground" Value="#7B4F00"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding LevelN}" Value="5">
          <Setter Property="Background" Value="#F5F5F5"/>
          <Setter Property="Foreground" Value="#555555"/>
        </DataTrigger>
      </Style.Triggers>
    </Style>

  </Window.Resources>

  <DockPanel LastChildFill="True">

    <!-- ===== Header toolbar ===== -->
    <Border DockPanel.Dock="Top" Background="#1E1E2E" Padding="10,8">
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="Auto"/>
          <ColumnDefinition Width="*"/>
          <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>

        <!-- Logo + title -->
        <StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
          <Viewbox Width="28" Height="28" Margin="0,0,9,0">
            <Canvas Width="32" Height="32">
              <Rectangle Width="32" Height="32" RadiusX="5" RadiusY="5" Fill="#0078D4"/>
              <!-- Hex package -->
              <Polygon Points="16,4 25,9 25,19 16,24 7,19 7,9"
                       Stroke="White" StrokeThickness="1.5" Fill="Transparent"/>
              <Line X1="7" Y1="9" X2="16" Y2="14" Stroke="White" StrokeThickness="1.5"/>
              <Line X1="25" Y1="9" X2="16" Y2="14" Stroke="White" StrokeThickness="1.5"/>
              <Line X1="16" Y1="14" X2="16" Y2="24" Stroke="White" StrokeThickness="1.5"/>
              <!-- Magnifier -->
              <Ellipse Canvas.Left="18" Canvas.Top="19" Width="9" Height="9"
                       Stroke="White" StrokeThickness="1.5" Fill="#1E1E2E"/>
              <Line X1="26" Y1="27" X2="31" Y2="32"
                    Stroke="White" StrokeThickness="2"
                    StrokeStartLineCap="Round" StrokeEndLineCap="Round"/>
            </Canvas>
          </Viewbox>
          <TextBlock Text="NIT MSIX EventlogTracer"
                     Foreground="White" FontSize="15" FontWeight="SemiBold"
                     VerticalAlignment="Center"/>
        </StackPanel>

        <!-- Centre: Start/Stop + package filter + recording clock -->
        <StackPanel Grid.Column="1" Orientation="Horizontal"
                    HorizontalAlignment="Center" VerticalAlignment="Center">
          <Button Name="BtnStartStop" Style="{StaticResource StartBtn}"
                  Content="Start Recording" ToolTip="F5"/>
          <TextBlock Text="Package filter:" Foreground="#AAAAAA"
                     VerticalAlignment="Center" Margin="0,0,5,0"/>
          <TextBox Name="TxtPackageFilter" Width="155" Padding="6,3"
                   Background="#2D2D3F" Foreground="White" CaretBrush="White"
                   BorderBrush="#555" VerticalAlignment="Center"
                   ToolTip="Optional package name to restrict event collection (e.g. WinRAR)"/>
          <TextBlock Name="TxtRecordClock"
                     Foreground="#55BBFF" FontFamily="Consolas" FontSize="14"
                     FontWeight="SemiBold" VerticalAlignment="Center"
                     Margin="14,0,0,0" Text=""/>
        </StackPanel>

        <!-- Right: action buttons -->
        <StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
          <Button Name="BtnCopyAll" Style="{StaticResource DarkBtn}"
                  Content="Copy All" IsEnabled="False"
                  ToolTip="Copy all visible events to clipboard"/>
          <Button Name="BtnCopySelected" Style="{StaticResource DarkBtn}"
                  Content="Copy Sel." IsEnabled="False"
                  ToolTip="Copy selected rows to clipboard"/>
          <Button Name="BtnSave" Style="{StaticResource DarkBtn}"
                  Content="Save..." IsEnabled="False"
                  ToolTip="Save events to text or CSV file"/>
          <Button Name="BtnClear" Style="{StaticResource DarkBtn}"
                  Content="Clear" IsEnabled="False" Margin="0"
                  ToolTip="Discard collected events and return to ready state"/>
        </StackPanel>
      </Grid>
    </Border>

    <!-- ===== Filter bar (hidden until results available) ===== -->
    <Border Name="FilterBar" DockPanel.Dock="Top" Visibility="Collapsed"
            Background="#EAEAEA" BorderBrush="#D0D0D0" BorderThickness="0,0,0,1"
            Padding="10,5">
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="Search:" VerticalAlignment="Center" Margin="0,0,6,0" Foreground="#444"/>
        <TextBox Name="TxtFilter" Width="290" Padding="5,3"
                 BorderBrush="#BBBBBB" VerticalAlignment="Center"
                 ToolTip="Filter across all fields (Ctrl+F to focus, Escape to clear)"/>
        <Button Name="BtnClearFilter" Content=" X " Style="{StaticResource SmallBtn}"
                Margin="4,0,12,0" VerticalAlignment="Center" ToolTip="Clear filter"/>
        <Rectangle Width="1" Fill="#C0C0C0" Margin="0,2" VerticalAlignment="Stretch"/>
        <TextBlock Text="Level:" VerticalAlignment="Center" Foreground="#444" Margin="12,0,6,0"/>
        <ComboBox Name="CboLevel" Width="130" VerticalAlignment="Center" Padding="4,2"
                  SelectedIndex="0">
          <ComboBoxItem Content="All levels"/>
          <ComboBoxItem Content="Error + Warning"/>
          <ComboBoxItem Content="Error only"/>
        </ComboBox>
        <TextBlock Name="TxtFilterCount" VerticalAlignment="Center"
                   Foreground="#666" Margin="16,0,0,0" FontStyle="Italic"/>
      </StackPanel>
    </Border>

    <!-- ===== Status bar ===== -->
    <Border DockPanel.Dock="Bottom" Background="#1E1E2E" Padding="10,4">
      <Grid>
        <TextBlock Name="TxtStatus" Foreground="#9999AA" VerticalAlignment="Center"
                   Text="Ready. Run as Administrator for full diagnostic channel access."/>
        <StackPanel HorizontalAlignment="Right" Orientation="Horizontal"
                    VerticalAlignment="Center">
          <Ellipse Name="RecordingDot" Width="8" Height="8" Fill="#E81123"
                   Margin="0,0,6,0" Visibility="Collapsed"/>
          <TextBlock Name="TxtEventCount" Foreground="#9999AA"/>
        </StackPanel>
      </Grid>
    </Border>

    <!-- ===== Main content area ===== -->
    <Grid Name="MainGrid">
      <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="5"/>
        <RowDefinition Height="175" MinHeight="60"/>
      </Grid.RowDefinitions>

      <!-- Recording / ready splash panel -->
      <Grid Name="RecordingPanel" Grid.Row="0" Background="White">
        <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">

          <Viewbox Width="80" Height="80" Margin="0,0,0,18" HorizontalAlignment="Center">
            <Canvas Width="60" Height="60">
              <Ellipse Width="60" Height="60" Fill="#E5F1FB" Stroke="#0078D4" StrokeThickness="2.5"/>
              <Polygon Canvas.Left="10" Canvas.Top="10"
                       Points="20,5 33,12 33,26 20,33 7,26 7,12"
                       Fill="#0078D4"/>
              <Polygon Canvas.Left="10" Canvas.Top="10"
                       Points="20,5 33,12 20,19"
                       Fill="#004F8B"/>
              <Line Canvas.Left="10" Canvas.Top="10"
                    X1="20" Y1="19" X2="20" Y2="33"
                    Stroke="#80BFFF" StrokeThickness="1.5"/>
              <Line Canvas.Left="10" Canvas.Top="10"
                    X1="7" Y1="12" X2="20" Y2="19"
                    Stroke="#80BFFF" StrokeThickness="1.5"/>
            </Canvas>
          </Viewbox>

          <TextBlock Name="TxtSplashStatus"
                     Text="Ready to record MSIX deployment events"
                     FontSize="17" FontWeight="SemiBold"
                     Foreground="#1E1E2E" HorizontalAlignment="Center"/>

          <TextBlock Name="TxtSplashSub"
                     Text="Press 'Start Recording', perform your MSIX operation, then press Stop."
                     FontSize="12" Foreground="#666"
                     HorizontalAlignment="Center" Margin="0,7,0,0"/>

          <ProgressBar Name="RecordProgress"
                       Height="5" Width="400" Margin="0,22,0,0"
                       IsIndeterminate="False" Value="0"
                       Foreground="#0078D4" Background="#DDDDDD"
                       Visibility="Collapsed"/>

          <TextBlock Text="Monitored channels:" HorizontalAlignment="Center"
                     Foreground="#888" Margin="0,26,0,5" FontSize="11"/>
          <ItemsControl Name="ChannelList" HorizontalAlignment="Center">
            <ItemsControl.ItemTemplate>
              <DataTemplate>
                <TextBlock Text="{Binding}" Foreground="#777" FontSize="10"
                           HorizontalAlignment="Center"/>
              </DataTemplate>
            </ItemsControl.ItemTemplate>
          </ItemsControl>

        </StackPanel>
      </Grid>

      <!-- Event DataGrid (shown after collection, overlaps RecordingPanel in Row 0) -->
      <DataGrid Name="EventGrid"
                Grid.Row="0"
                Visibility="Collapsed"
                AutoGenerateColumns="False"
                IsReadOnly="True"
                SelectionMode="Extended"
                GridLinesVisibility="Horizontal"
                HeadersVisibility="Column"
                RowStyle="{StaticResource EventRowStyle}"
                AlternatingRowBackground="#F9F9F9"
                CanUserReorderColumns="True"
                CanUserResizeColumns="True"
                CanUserSortColumns="True"
                HorizontalScrollBarVisibility="Auto"
                VerticalScrollBarVisibility="Auto"
                FontFamily="Consolas" FontSize="11.5">
        <DataGrid.Columns>
          <DataGridTextColumn Header="Time" Binding="{Binding TimeStr}" Width="155" SortMemberPath="Time"/>
          <DataGridTextColumn Header="Level" Binding="{Binding Level}" Width="78"/>
          <DataGridTextColumn Header="Id" Binding="{Binding EventId}" Width="46"/>
          <DataGridTextColumn Header="Channel" Binding="{Binding ChannelShort}" Width="185"/>
          <DataGridTextColumn Header="Message" Binding="{Binding MessageShort}" Width="*">
            <DataGridTextColumn.ElementStyle>
              <Style TargetType="TextBlock">
                <Setter Property="TextTrimming" Value="CharacterEllipsis"/>
              </Style>
            </DataGridTextColumn.ElementStyle>
          </DataGridTextColumn>
        </DataGrid.Columns>
        <DataGrid.ContextMenu>
          <ContextMenu>
            <MenuItem Name="MenuCopyRow" Header="Copy Row"/>
            <MenuItem Name="MenuCopyMessage" Header="Copy Full Message"/>
            <Separator/>
            <MenuItem Name="MenuCopyAll" Header="Copy All Visible Rows"/>
          </ContextMenu>
        </DataGrid.ContextMenu>
      </DataGrid>

      <!-- GridSplitter -->
      <GridSplitter Grid.Row="1" HorizontalAlignment="Stretch" Background="#C8C8C8"
                    ResizeBehavior="PreviousAndNext" Cursor="SizeNS"/>

      <!-- Details panel -->
      <Grid Grid.Row="2" Background="White">
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Border Grid.Row="0" Background="#E8E8E8"
                BorderBrush="#D0D0D0" BorderThickness="0,1,0,1"
                Padding="8,4">
          <Grid>
            <TextBlock Text="Event Details" FontWeight="SemiBold" Foreground="#333"
                       VerticalAlignment="Center"/>
            <StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
              <Button Name="BtnCopyDetail" Style="{StaticResource SmallBtn}"
                      Content="Copy Details" ToolTip="Copy full details to clipboard"/>
              <Button Name="BtnCopyField" Style="{StaticResource SmallBtn}"
                      Content="Copy Field..." Margin="0"
                      ToolTip="Choose a single field to copy"/>
            </StackPanel>
          </Grid>
        </Border>
        <TextBox Grid.Row="1" Name="TxtDetails"
                 IsReadOnly="True" TextWrapping="Wrap"
                 VerticalScrollBarVisibility="Auto"
                 HorizontalScrollBarVisibility="Disabled"
                 FontFamily="Consolas" FontSize="11"
                 Padding="10" Background="White" BorderThickness="0"
                 Text="Select an event to see details."/>
      </Grid>

    </Grid>
  </DockPanel>
</Window>
'@


# ---------------------------------------------------------------------------
# Load and bind window
# ---------------------------------------------------------------------------
$reader = New-Object System.Xml.XmlNodeReader($xaml)
$window = [System.Windows.Markup.XamlReader]::Load($reader)

$BtnStartStop    = $window.FindName('BtnStartStop')
$TxtPackageFilter= $window.FindName('TxtPackageFilter')
$TxtRecordClock  = $window.FindName('TxtRecordClock')
$BtnCopyAll      = $window.FindName('BtnCopyAll')
$BtnCopySelected = $window.FindName('BtnCopySelected')
$BtnSave         = $window.FindName('BtnSave')
$BtnClear        = $window.FindName('BtnClear')
$FilterBar       = $window.FindName('FilterBar')
$TxtFilter       = $window.FindName('TxtFilter')
$BtnClearFilter  = $window.FindName('BtnClearFilter')
$CboLevel        = $window.FindName('CboLevel')
$TxtFilterCount  = $window.FindName('TxtFilterCount')
$TxtStatus       = $window.FindName('TxtStatus')
$TxtEventCount   = $window.FindName('TxtEventCount')
$RecordingDot    = $window.FindName('RecordingDot')
$RecordingPanel  = $window.FindName('RecordingPanel')
$EventGrid       = $window.FindName('EventGrid')
$RecordProgress  = $window.FindName('RecordProgress')
$TxtSplashStatus = $window.FindName('TxtSplashStatus')
$TxtSplashSub    = $window.FindName('TxtSplashSub')
$TxtDetails      = $window.FindName('TxtDetails')
$BtnCopyDetail   = $window.FindName('BtnCopyDetail')
$BtnCopyField    = $window.FindName('BtnCopyField')
$ChannelList     = $window.FindName('ChannelList')
$MenuCopyRow     = $window.FindName('MenuCopyRow')
$MenuCopyMessage = $window.FindName('MenuCopyMessage')
$MenuCopyAll     = $window.FindName('MenuCopyAll')

# Set window icon
$window.Icon = New-WindowIcon

# Populate channel list in splash panel
$ChannelList.ItemsSource = $script:Channels

# Admin warning
if (-not (Test-IsAdmin)) {
    $TxtStatus.Text       = 'Not running as Administrator -- diagnostic channels may be unavailable (read-only access only).'
    $TxtStatus.Foreground = New-Object System.Windows.Media.SolidColorBrush(
        [System.Windows.Media.Color]::FromRgb(220, 160, 0))
}

# ---------------------------------------------------------------------------
# Event handlers
# ---------------------------------------------------------------------------

$BtnStartStop.Add_Click({
    if (-not $script:IsRecording) {
        # ---- Start recording ----
        $script:IsRecording = $true
        $script:StartTime   = Get-Date
        $script:AllEvents.Clear()

        $BtnStartStop.Content    = 'Stop Recording'
        $BtnStartStop.Background = New-Object System.Windows.Media.SolidColorBrush(
            [System.Windows.Media.Color]::FromRgb(209, 52, 56))

        $TxtSplashStatus.Text           = 'Recording MSIX events...'
        $TxtSplashSub.Text              = 'Perform your MSIX operation now. Press Stop when done.'
        $RecordProgress.Visibility      = [System.Windows.Visibility]::Visible
        $RecordProgress.IsIndeterminate = $true
        $RecordingDot.Visibility        = [System.Windows.Visibility]::Visible

        # Ensure splash is shown even if results were displayed before
        $RecordingPanel.Visibility = [System.Windows.Visibility]::Visible
        $EventGrid.Visibility      = [System.Windows.Visibility]::Collapsed
        $FilterBar.Visibility      = [System.Windows.Visibility]::Collapsed
        $BtnCopyAll.IsEnabled      = $false
        $BtnCopySelected.IsEnabled = $false
        $BtnSave.IsEnabled         = $false
        $BtnClear.IsEnabled        = $false

        $TxtStatus.Text       = "Recording started at $($script:StartTime.ToString('HH:mm:ss')) -- perform your MSIX operation now."
        $TxtStatus.Foreground = New-Object System.Windows.Media.SolidColorBrush(
            [System.Windows.Media.Color]::FromRgb(85, 187, 255))
        $TxtEventCount.Text   = ''

        Enable-MSIXChannels

        # Clock timer: updates elapsed time every second
        $script:ClockTimer          = New-Object System.Windows.Threading.DispatcherTimer
        $script:ClockTimer.Interval = [TimeSpan]::FromSeconds(1)
        $script:ClockTimer.Add_Tick({
            if ($script:StartTime) {
                $elapsed = (Get-Date) - $script:StartTime
                $TxtRecordClock.Text = $elapsed.ToString('hh\:mm\:ss')
            }
        })
        $script:ClockTimer.Start()
    }
    else {
        # ---- Stop recording ----
        $script:EndTime     = Get-Date
        $script:IsRecording = $false

        if ($script:ClockTimer) { $script:ClockTimer.Stop(); $script:ClockTimer = $null }
        $TxtRecordClock.Text = ''

        $BtnStartStop.Content    = 'Start Recording'
        $BtnStartStop.Background = New-Object System.Windows.Media.SolidColorBrush(
            [System.Windows.Media.Color]::FromRgb(0, 120, 212))

        $RecordProgress.IsIndeterminate = $false
        $RecordProgress.Visibility      = [System.Windows.Visibility]::Collapsed
        $RecordingDot.Visibility        = [System.Windows.Visibility]::Collapsed

        $TxtSplashStatus.Text = 'Collecting events, please wait...'
        $TxtSplashSub.Text    = 'Reading event channels...'
        $TxtStatus.Text       = "Collecting events from $($script:StartTime.ToString('HH:mm:ss')) to $($script:EndTime.ToString('HH:mm:ss'))..."
        $TxtStatus.Foreground = New-Object System.Windows.Media.SolidColorBrush(
            [System.Windows.Media.Color]::FromRgb(153, 153, 170))

        # Force UI update before the potentially slow event query
        [System.Windows.Forms.Application]::DoEvents()

        $events = Get-MSIXEvents -Since $script:StartTime -Until $script:EndTime
        foreach ($ev in $events) { $null = $script:AllEvents.Add($ev) }

        $count = $script:AllEvents.Count
        $TxtStatus.Text = "Collected $count event(s) between $($script:StartTime.ToString('HH:mm:ss')) and $($script:EndTime.ToString('HH:mm:ss'))."

        if ($count -eq 0) {
            $TxtSplashStatus.Text = 'No events found in the recording window.'
            $TxtSplashSub.Text    = 'Ensure diagnostic channels are enabled and the MSIX operation was performed during recording.'
        }
        else {
            Show-Results
        }
    }
})

$BtnCopyAll.Add_Click({
    $items = @($EventGrid.ItemsSource)
    if ($items.Count -eq 0) { return }
    [System.Windows.Clipboard]::SetText((Format-EventsAsText -Events $items))
    $TxtStatus.Text = "Copied $($items.Count) event(s) to clipboard."
})

$BtnCopySelected.Add_Click({
    $items = @($EventGrid.SelectedItems)
    if ($items.Count -eq 0) {
        $TxtStatus.Text = 'No rows selected.'
        return
    }
    [System.Windows.Clipboard]::SetText((Format-EventsAsText -Events $items))
    $TxtStatus.Text = "Copied $($items.Count) selected event(s) to clipboard."
})

$BtnSave.Add_Click({
    $dlg            = New-Object Microsoft.Win32.SaveFileDialog
    $dlg.Title      = 'Save MSIX Events'
    $dlg.Filter     = 'Text files (*.txt)|*.txt|CSV files (*.csv)|*.csv|All files (*.*)|*.*'
    $dlg.FileName   = "MSIX_Events_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
    if ($dlg.ShowDialog($window) -eq $true) {
        $items = @($EventGrid.ItemsSource)
        try {
            if ($dlg.FilterIndex -eq 2) {
                Format-EventsAsCsv -Events $items | Set-Content -Path $dlg.FileName -Encoding UTF8
            }
            else {
                Format-EventsAsText -Events $items | Set-Content -Path $dlg.FileName -Encoding UTF8
            }
            $TxtStatus.Text = "Saved $($items.Count) event(s) to: $($dlg.FileName)"
        }
        catch {
            [System.Windows.MessageBox]::Show("Save failed: $_", 'NIT MSIX EventlogTracer',
                [System.Windows.MessageBoxButton]::OK,
                [System.Windows.MessageBoxImage]::Error) | Out-Null
        }
    }
})

$BtnClear.Add_Click({
    $res = [System.Windows.MessageBox]::Show(
        'Discard all collected events and return to ready state?',
        'NIT MSIX EventlogTracer',
        [System.Windows.MessageBoxButton]::YesNo,
        [System.Windows.MessageBoxImage]::Question)
    if ($res -eq [System.Windows.MessageBoxResult]::Yes) {
        Reset-ToReady
        $TxtStatus.Text       = 'Events cleared.'
        $TxtStatus.Foreground = New-Object System.Windows.Media.SolidColorBrush(
            [System.Windows.Media.Color]::FromRgb(153, 153, 170))
    }
})

$TxtFilter.Add_TextChanged({ Update-EventFilter })
$CboLevel.Add_SelectionChanged({ Update-EventFilter })

$BtnClearFilter.Add_Click({
    $TxtFilter.Text         = ''
    $CboLevel.SelectedIndex = 0
})

$EventGrid.Add_SelectionChanged({
    $sel = $EventGrid.SelectedItem
    $TxtDetails.Text = Get-DetailText -Row $sel
})

# Select row on right-click so context menu reflects the clicked row
$EventGrid.Add_PreviewMouseRightButtonDown({
    param($sender, $e)
    $dep = [System.Windows.DependencyObject] $e.OriginalSource
    while ($null -ne $dep -and -not ($dep -is [System.Windows.Controls.DataGridRow])) {
        $dep = [System.Windows.Media.VisualTreeHelper]::GetParent($dep)
    }
    if ($null -ne $dep) {
        $dep.IsSelected = $true
    }
})

$BtnCopyDetail.Add_Click({
    if ($TxtDetails.Text -ne 'Select an event to see details.') {
        [System.Windows.Clipboard]::SetText($TxtDetails.Text)
        $TxtStatus.Text = 'Event details copied to clipboard.'
    }
})

$BtnCopyField.Add_Click({
    $sel = $EventGrid.SelectedItem
    if ($null -eq $sel) {
        $TxtStatus.Text = 'Select an event first.'
        return
    }

    $fields = @(
        [PSCustomObject]@{ Label = 'Time';        Value = $sel.TimeStr }
        [PSCustomObject]@{ Label = 'Level';       Value = $sel.Level }
        [PSCustomObject]@{ Label = 'Event Id';    Value = [string]$sel.EventId }
        [PSCustomObject]@{ Label = 'Channel';     Value = $sel.Channel }
        [PSCustomObject]@{ Label = 'Provider';    Value = $sel.Provider }
        [PSCustomObject]@{ Label = 'Task';        Value = $sel.Task }
        [PSCustomObject]@{ Label = 'Keywords';    Value = $sel.Keywords }
        [PSCustomObject]@{ Label = 'Activity Id'; Value = $sel.ActivityId }
        [PSCustomObject]@{ Label = 'Full Message';Value = $sel.FullMessage }
    )

    $popup = New-Object System.Windows.Window
    $popup.Title                 = 'Copy Field'
    $popup.Width                 = 340
    $popup.Height                = 300
    $popup.ResizeMode            = [System.Windows.ResizeMode]::NoResize
    $popup.WindowStartupLocation = [System.Windows.WindowStartupLocation]::CenterOwner
    $popup.Owner                 = $window
    $popup.FontFamily            = New-Object System.Windows.Media.FontFamily('Segoe UI')

    $sp = New-Object System.Windows.Controls.StackPanel
    $sp.Margin = New-Object System.Windows.Thickness(10)

    $lbl = New-Object System.Windows.Controls.TextBlock
    $lbl.Text   = 'Double-click a field to copy it:'
    $lbl.Margin = New-Object System.Windows.Thickness(0, 0, 0, 6)
    $null = $sp.Children.Add($lbl)

    $lb = New-Object System.Windows.Controls.ListBox
    $lb.Height = 190
    foreach ($f in $fields) {
        $preview = $f.Value
        if ($preview.Length -gt 60) { $preview = $preview.Substring(0, 60) + '...' }
        $item         = New-Object System.Windows.Controls.ListBoxItem
        $item.Content = "$($f.Label): $preview"
        $item.Tag     = $f.Value
        $item.ToolTip = $f.Value
        $null = $lb.Items.Add($item)
    }
    $null = $sp.Children.Add($lb)

    # Double-click copies immediately
    $lb.Add_MouseDoubleClick({
        $chosen = $lb.SelectedItem
        if ($null -ne $chosen -and $chosen.Tag -ne '') {
            [System.Windows.Clipboard]::SetText($chosen.Tag)
            $TxtStatus.Text = "Copied field '$($chosen.Content.Split(':')[0])'."
            $popup.Close()
        }
    })

    $btnCopy = New-Object System.Windows.Controls.Button
    $btnCopy.Content = 'Copy Selected Field'
    $btnCopy.Margin  = New-Object System.Windows.Thickness(0, 8, 0, 0)
    $btnCopy.Padding = New-Object System.Windows.Thickness(10, 4, 10, 4)
    $btnCopy.Cursor  = [System.Windows.Input.Cursors]::Hand
    $btnCopy.Add_Click({
        $chosen = $lb.SelectedItem
        if ($null -ne $chosen -and $chosen.Tag -ne '') {
            [System.Windows.Clipboard]::SetText($chosen.Tag)
            $TxtStatus.Text = "Copied field '$($chosen.Content.Split(':')[0])'."
        }
        $popup.Close()
    })
    $null = $sp.Children.Add($btnCopy)

    $popup.Content = $sp
    $null = $popup.ShowDialog()
})

# Context menu handlers
$MenuCopyRow.Add_Click({
    $sel = $EventGrid.SelectedItem
    if ($null -eq $sel) { return }
    $text = "$($sel.TimeStr) [$($sel.Level)] Id=$($sel.EventId) $($sel.Channel)`r`n$($sel.FullMessage)"
    [System.Windows.Clipboard]::SetText($text)
    $TxtStatus.Text = 'Row copied to clipboard.'
})

$MenuCopyMessage.Add_Click({
    $sel = $EventGrid.SelectedItem
    if ($null -eq $sel) { return }
    [System.Windows.Clipboard]::SetText($sel.FullMessage)
    $TxtStatus.Text = 'Message copied to clipboard.'
})

$MenuCopyAll.Add_Click({
    $items = @($EventGrid.ItemsSource)
    if ($items.Count -eq 0) { return }
    [System.Windows.Clipboard]::SetText((Format-EventsAsText -Events $items))
    $TxtStatus.Text = "Copied $($items.Count) visible event(s) to clipboard."
})

# Keyboard shortcuts
$window.Add_KeyDown({
    param($sender, $e)
    switch ($e.Key) {
        'F5' {
            $BtnStartStop.RaiseEvent(
                (New-Object System.Windows.RoutedEventArgs(
                    [System.Windows.Controls.Button]::ClickEvent)))
            $e.Handled = $true
        }
        'Escape' {
            if ($TxtFilter.Text -ne '') {
                $TxtFilter.Text = ''
                $e.Handled = $true
            }
        }
        'F' {
            if ($e.KeyboardDevice.Modifiers -eq [System.Windows.Input.ModifierKeys]::Control) {
                $null = $TxtFilter.Focus()
                $e.Handled = $true
            }
        }
    }
})

$window.Add_Closing({
    if ($script:ClockTimer) { $script:ClockTimer.Stop() }
})

# ---------------------------------------------------------------------------
# Show window
# ---------------------------------------------------------------------------
Add-Type -AssemblyName System.Windows.Forms
[void] $window.ShowDialog()