diff --git a/FatVerificationWindow.cs b/FatVerificationWindow.cs
new file mode 100644
index 000000000..1147b02fd
--- /dev/null
+++ b/FatVerificationWindow.cs
@@ -0,0 +1,579 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Input;
+using System.Windows.Media;
+using ArIED61850Tester.Models.IoTesting;
+using ArIED61850Tester.Services.IoTesting;
+
+namespace ArIED61850Tester;
+
+///
+/// Additive FAT v2 workspace. It deliberately does not reuse the legacy ON/OFF grid because
+/// digital, analog, and fallback DataSet members all retain generic Value 1 / Value 2 semantics.
+///
+public sealed class FatVerificationWindow : Window
+{
+ private readonly FatSclWorkspaceLaunchResult _launch;
+ private readonly FatLiveCaptureCoordinator _capture;
+ private readonly Dictionary _rowsById;
+ private readonly TextBox _searchBox = new();
+ private readonly TextBlock _summary = new();
+ private readonly Button _removedButton = new();
+ private readonly DataGrid _grid = new();
+
+ public FatVerificationWindow(FatSclWorkspaceLaunchResult launch)
+ {
+ _launch = launch ?? throw new ArgumentNullException(nameof(launch));
+ _capture = new FatLiveCaptureCoordinator(launch.Project);
+ _rowsById = launch.Project.Signals.ToDictionary(
+ signal => signal.SignalId,
+ signal => new FatVerificationRow(signal, _capture, ShowCaptureError),
+ StringComparer.OrdinalIgnoreCase);
+
+ Title = "ARSAS FAT v2 — DataSet Verification";
+ Width = 1440;
+ Height = 800;
+ MinWidth = 1040;
+ MinHeight = 620;
+ WindowStartupLocation = WindowStartupLocation.CenterOwner;
+ Background = Brush("#F4F7FB");
+ Content = BuildLayout();
+ RefreshRows();
+ }
+
+ public long EvidenceHistoryCount => _capture.History.Count;
+
+ ///
+ /// Receives the existing monitor-runtime image. No second polling loop is created for FAT.
+ /// Exact runtime reference plus IED identity controls fan-out to static membership rows.
+ ///
+ public void ApplyLiveObservation(
+ string runtimeReference,
+ IEnumerable iedAliases,
+ string previousRawValue,
+ bool isValueEdge,
+ FatLiveValueObservation observation)
+ {
+ var aliases = iedAliases?.ToArray() ?? Array.Empty();
+ if (!Dispatcher.CheckAccess())
+ {
+ Dispatcher.BeginInvoke(new Action(() => ApplyLiveObservation(
+ runtimeReference,
+ aliases,
+ previousRawValue,
+ isValueEdge,
+ observation)));
+ return;
+ }
+
+ var matched = _capture.Observe(
+ runtimeReference,
+ aliases,
+ previousRawValue,
+ isValueEdge,
+ observation);
+ foreach (var signal in matched)
+ {
+ if (_rowsById.TryGetValue(signal.SignalId, out var row))
+ row.Refresh();
+ }
+
+ if (matched.Count > 0)
+ RefreshSummary();
+ }
+
+ private UIElement BuildLayout()
+ {
+ var root = new Grid { Margin = new Thickness(20) };
+ root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
+
+ var heading = new Grid { Margin = new Thickness(0, 0, 0, 14) };
+ heading.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ heading.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ var title = new StackPanel();
+ title.Children.Add(new TextBlock
+ {
+ Text = "FAT v2 · STATIC DATASET VERIFICATION",
+ FontSize = 11,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = Brush("#2563EB")
+ });
+ title.Children.Add(new TextBlock
+ {
+ Text = "Value 1 / Value 2 evidence workspace",
+ FontSize = 24,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = Brush("#172033"),
+ Margin = new Thickness(0, 3, 0, 3)
+ });
+ title.Children.Add(_summary);
+ Grid.SetColumn(title, 0);
+ heading.Children.Add(title);
+
+ var sourceBadge = new Border
+ {
+ Padding = new Thickness(12, 8, 12, 8),
+ CornerRadius = new CornerRadius(10),
+ Background = Brushes.White,
+ BorderBrush = Brush("#D9E2EF"),
+ BorderThickness = new Thickness(1),
+ Child = new TextBlock
+ {
+ Text = $"{_launch.SourceFiles.Count} SCL source(s) · {_launch.SourceSetSha256[..12]}",
+ FontSize = 12,
+ Foreground = Brush("#4B5D73")
+ }
+ };
+ Grid.SetColumn(sourceBadge, 1);
+ heading.Children.Add(sourceBadge);
+ Grid.SetRow(heading, 0);
+ root.Children.Add(heading);
+
+ var toolbar = new Grid { Margin = new Thickness(0, 0, 0, 10) };
+ toolbar.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ toolbar.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ _searchBox.Height = 34;
+ _searchBox.MaxWidth = 560;
+ _searchBox.HorizontalAlignment = HorizontalAlignment.Left;
+ _searchBox.VerticalContentAlignment = VerticalAlignment.Center;
+ _searchBox.Padding = new Thickness(10, 0, 10, 0);
+ _searchBox.ToolTip = "Search IED, DataSet, signal, FC, type, or current value";
+ _searchBox.TextChanged += (_, _) => RefreshRows();
+ Grid.SetColumn(_searchBox, 0);
+ toolbar.Children.Add(_searchBox);
+
+ _removedButton.MinWidth = 150;
+ _removedButton.Height = 34;
+ _removedButton.Padding = new Thickness(12, 0, 12, 0);
+ _removedButton.Click += RemovedSignals_Click;
+ Grid.SetColumn(_removedButton, 1);
+ toolbar.Children.Add(_removedButton);
+ Grid.SetRow(toolbar, 1);
+ root.Children.Add(toolbar);
+
+ _grid.AutoGenerateColumns = false;
+ _grid.IsReadOnly = true;
+ _grid.CanUserAddRows = false;
+ _grid.CanUserDeleteRows = false;
+ _grid.SelectionMode = DataGridSelectionMode.Single;
+ _grid.SelectionUnit = DataGridSelectionUnit.FullRow;
+ _grid.HeadersVisibility = DataGridHeadersVisibility.Column;
+ _grid.GridLinesVisibility = DataGridGridLinesVisibility.Horizontal;
+ _grid.BorderBrush = Brush("#D9E2EF");
+ _grid.BorderThickness = new Thickness(1);
+ _grid.Background = Brushes.White;
+ _grid.RowBackground = Brushes.White;
+ _grid.AlternatingRowBackground = Brush("#FAFCFF");
+ _grid.RowHeight = 36;
+ _grid.ColumnHeaderHeight = 36;
+ _grid.PreviewMouseRightButtonDown += Grid_PreviewMouseRightButtonDown;
+ _grid.Columns.Add(TextColumn("IED", nameof(FatVerificationRow.IedName), 115));
+ _grid.Columns.Add(TextColumn("DataSet", nameof(FatVerificationRow.DataSetReference), 190));
+ _grid.Columns.Add(TextColumn("#", nameof(FatVerificationRow.DataSetMemberIndex), 42));
+ _grid.Columns.Add(TextColumn("Signal", nameof(FatVerificationRow.StaticMemberReference), 300));
+ _grid.Columns.Add(TextColumn("FC", nameof(FatVerificationRow.FunctionalConstraint), 48));
+ _grid.Columns.Add(TextColumn("Kind", nameof(FatVerificationRow.SignalKind), 75));
+ _grid.Columns.Add(TextColumn("Current", nameof(FatVerificationRow.CurrentValue), 120));
+ _grid.Columns.Add(EvidenceColumn(
+ "Value 1",
+ nameof(FatVerificationRow.Value1Display),
+ nameof(FatVerificationRow.CaptureValue1Command),
+ 155));
+ _grid.Columns.Add(EvidenceColumn(
+ "Value 2",
+ nameof(FatVerificationRow.Value2Display),
+ nameof(FatVerificationRow.CaptureValue2Command),
+ 155));
+ var remove = new MenuItem { Header = "Remove from FAT" };
+ remove.Click += RemoveSelected_Click;
+ _grid.ContextMenu = new ContextMenu { Items = { remove } };
+
+ Grid.SetRow(_grid, 2);
+ root.Children.Add(_grid);
+ return root;
+ }
+
+ private static DataGridTextColumn TextColumn(string header, string path, double width)
+ => new()
+ {
+ Header = header,
+ Binding = new Binding(path),
+ Width = new DataGridLength(width)
+ };
+
+ private static DataGridTemplateColumn EvidenceColumn(
+ string header,
+ string valuePath,
+ string commandPath,
+ double width)
+ {
+ var panel = new FrameworkElementFactory(typeof(StackPanel));
+ panel.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);
+ panel.SetValue(FrameworkElement.VerticalAlignmentProperty, VerticalAlignment.Center);
+
+ var value = new FrameworkElementFactory(typeof(TextBlock));
+ value.SetValue(TextBlock.VerticalAlignmentProperty, VerticalAlignment.Center);
+ value.SetValue(TextBlock.MinWidthProperty, 84d);
+ value.SetBinding(TextBlock.TextProperty, new Binding(valuePath));
+ panel.AppendChild(value);
+
+ var capture = new FrameworkElementFactory(typeof(Button));
+ capture.SetValue(ContentControl.ContentProperty, "✓");
+ capture.SetValue(FrameworkElement.WidthProperty, 28d);
+ capture.SetValue(FrameworkElement.HeightProperty, 24d);
+ capture.SetValue(FrameworkElement.MarginProperty, new Thickness(5, 0, 0, 0));
+ capture.SetValue(FrameworkElement.ToolTipProperty, $"Capture current live reading as {header}");
+ capture.SetBinding(Button.CommandProperty, new Binding(commandPath));
+ capture.SetBinding(UIElement.VisibilityProperty, new Binding(nameof(FatVerificationRow.ManualCaptureVisibility)));
+ panel.AppendChild(capture);
+
+ return new DataGridTemplateColumn
+ {
+ Header = header,
+ Width = new DataGridLength(width),
+ CellTemplate = new DataTemplate { VisualTree = panel }
+ };
+ }
+
+ private void RefreshRows()
+ {
+ foreach (var row in _rowsById.Values)
+ row.Refresh();
+
+ var query = _searchBox.Text.Trim();
+ IEnumerable rows = _rowsById.Values
+ .Where(row => row.Signal.IsIncludedInFat);
+ if (query.Length > 0)
+ {
+ rows = rows.Where(row =>
+ Contains(row.IedName, query) ||
+ Contains(row.AccessPointName, query) ||
+ Contains(row.DataSetReference, query) ||
+ Contains(row.StaticMemberReference, query) ||
+ Contains(row.RuntimeReference, query) ||
+ Contains(row.FunctionalConstraint, query) ||
+ Contains(row.DataType, query) ||
+ Contains(row.SignalKind.ToString(), query) ||
+ Contains(row.CurrentValue, query));
+ }
+
+ _grid.ItemsSource = rows
+ .OrderBy(row => row.DataSetReference, StringComparer.OrdinalIgnoreCase)
+ .ThenBy(row => row.DataSetMemberIndex)
+ .ToArray();
+ RefreshSummary();
+ }
+
+ private void RefreshSummary()
+ {
+ var all = _launch.Project.Signals;
+ var live = all.Count(signal => _capture.GetLatestObservation(signal.SignalId) != null);
+ _summary.Text = $"{_launch.Project.IncludedSignals.Count} included · {_launch.Project.RemovedSignals.Count} removed · " +
+ $"{all.Count(signal => signal.SignalKind == FatSignalKind.Discrete)} digital · " +
+ $"{all.Count(signal => signal.SignalKind == FatSignalKind.Analog)} analog · " +
+ $"{all.Count(signal => signal.SignalKind == FatSignalKind.Other)} other · " +
+ $"{live} live · {_capture.History.Count} evidence history";
+ _summary.FontSize = 12.5;
+ _summary.Foreground = Brush("#64748B");
+ _removedButton.Content = $"Removed Signals ({_launch.Project.RemovedSignals.Count})";
+ }
+
+ private void RemoveSelected_Click(object sender, RoutedEventArgs e)
+ {
+ if (_grid.SelectedItem is not FatVerificationRow row)
+ return;
+ _launch.Project.RemoveSignal(row.Signal.SignalId);
+ row.Refresh();
+ RefreshRows();
+ }
+
+ private void RemovedSignals_Click(object sender, RoutedEventArgs e)
+ {
+ var dialog = new FatRemovedSignalsWindow(_launch.Project) { Owner = this };
+ dialog.ShowDialog();
+ RefreshRows();
+ }
+
+ private void ShowCaptureError(string message)
+ {
+ MessageBox.Show(
+ this,
+ message,
+ "FAT capture unavailable",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information);
+ }
+
+ private void Grid_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ var row = FindAncestor(e.OriginalSource as DependencyObject);
+ if (row != null)
+ _grid.SelectedItem = row.Item;
+ }
+
+ private static T? FindAncestor(DependencyObject? current) where T : DependencyObject
+ {
+ while (current != null)
+ {
+ if (current is T result)
+ return result;
+ current = VisualTreeHelper.GetParent(current);
+ }
+ return null;
+ }
+
+ private static bool Contains(string? value, string query)
+ => (value ?? string.Empty).Contains(query, StringComparison.OrdinalIgnoreCase);
+
+ private static Brush Brush(string value)
+ => new SolidColorBrush((Color)ColorConverter.ConvertFromString(value));
+
+ private sealed class FatVerificationRow : INotifyPropertyChanged
+ {
+ private readonly FatLiveCaptureCoordinator _capture;
+ private readonly Action _showError;
+ private readonly RelayCommand _captureValue1;
+ private readonly RelayCommand _captureValue2;
+
+ public FatVerificationRow(
+ FatVerificationSignal signal,
+ FatLiveCaptureCoordinator capture,
+ Action showError)
+ {
+ Signal = signal;
+ _capture = capture;
+ _showError = showError;
+ _captureValue1 = new RelayCommand(
+ () => Capture(FatValueSlot.Value1),
+ CanCapture);
+ _captureValue2 = new RelayCommand(
+ () => Capture(FatValueSlot.Value2),
+ CanCapture);
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public FatVerificationSignal Signal { get; }
+ public string IedName => Signal.IedName;
+ public string AccessPointName => Signal.AccessPointName;
+ public string DataSetReference => Signal.DataSetReference;
+ public int DataSetMemberIndex => Signal.DataSetMemberIndex;
+ public string StaticMemberReference => Signal.StaticMemberReference;
+ public string RuntimeReference => Signal.RuntimeReference;
+ public string FunctionalConstraint => Signal.FunctionalConstraint;
+ public string DataType => Signal.DataType;
+ public FatSignalKind SignalKind => Signal.SignalKind;
+ public string CurrentValue => _capture.GetLatestObservation(Signal.SignalId)?.RawValue ?? "—";
+ public string Value1Display => Signal.Value1Evidence?.RawValue ?? "—";
+ public string Value2Display => Signal.Value2Evidence?.RawValue ?? "—";
+ public Visibility ManualCaptureVisibility =>
+ Signal.CaptureMode == FatCaptureMode.OperatorSnapshot
+ ? Visibility.Visible
+ : Visibility.Collapsed;
+ public ICommand CaptureValue1Command => _captureValue1;
+ public ICommand CaptureValue2Command => _captureValue2;
+
+ public void Refresh()
+ {
+ Raise(nameof(CurrentValue));
+ Raise(nameof(Value1Display));
+ Raise(nameof(Value2Display));
+ Raise(nameof(ManualCaptureVisibility));
+ _captureValue1.RaiseCanExecuteChanged();
+ _captureValue2.RaiseCanExecuteChanged();
+ }
+
+ private bool CanCapture()
+ => Signal.IsIncludedInFat &&
+ Signal.CaptureMode == FatCaptureMode.OperatorSnapshot &&
+ _capture.GetLatestObservation(Signal.SignalId) != null;
+
+ private void Capture(FatValueSlot slot)
+ {
+ try
+ {
+ _capture.CaptureOperatorSnapshot(Signal.SignalId, slot);
+ Refresh();
+ }
+ catch (Exception ex) when (ex is InvalidOperationException or KeyNotFoundException)
+ {
+ _showError(ex.Message);
+ }
+ }
+
+ private void Raise([CallerMemberName] string? propertyName = null)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ private sealed class RelayCommand : ICommand
+ {
+ private readonly Action _execute;
+ private readonly Func _canExecute;
+
+ public RelayCommand(Action execute, Func canExecute)
+ {
+ _execute = execute ?? throw new ArgumentNullException(nameof(execute));
+ _canExecute = canExecute ?? throw new ArgumentNullException(nameof(canExecute));
+ }
+
+ public event EventHandler? CanExecuteChanged;
+ public bool CanExecute(object? parameter) => _canExecute();
+ public void Execute(object? parameter) => _execute();
+ public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+ }
+}
+
+internal sealed class FatRemovedSignalsWindow : Window
+{
+ private sealed class RemovedSignalRow
+ {
+ public required FatVerificationSignal Signal { get; init; }
+ public bool IsSelected { get; set; }
+ public string IedName => Signal.IedName;
+ public string DataSetReference => Signal.DataSetReference;
+ public int DataSetMemberIndex => Signal.DataSetMemberIndex;
+ public string StaticMemberReference => Signal.StaticMemberReference;
+ public FatSignalKind SignalKind => Signal.SignalKind;
+ public string SignalId => Signal.SignalId;
+ }
+
+ private readonly FatVerificationProject _project;
+ private readonly List _rows;
+ private readonly TextBox _search = new();
+ private readonly DataGrid _grid = new();
+
+ public FatRemovedSignalsWindow(FatVerificationProject project)
+ {
+ _project = project ?? throw new ArgumentNullException(nameof(project));
+ _rows = project.RemovedSignals.Select(signal => new RemovedSignalRow { Signal = signal }).ToList();
+ Title = "Removed Signals";
+ Width = 940;
+ Height = 560;
+ MinWidth = 760;
+ MinHeight = 460;
+ WindowStartupLocation = WindowStartupLocation.CenterOwner;
+ Content = BuildLayout();
+ RefreshRows();
+ }
+
+ private UIElement BuildLayout()
+ {
+ var root = new Grid { Margin = new Thickness(18) };
+ root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
+ root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+
+ var top = new DockPanel { Margin = new Thickness(0, 0, 0, 10) };
+ _search.Width = 380;
+ _search.Height = 32;
+ _search.VerticalContentAlignment = VerticalAlignment.Center;
+ _search.Padding = new Thickness(8, 0, 8, 0);
+ _search.ToolTip = "Search removed signals";
+ _search.TextChanged += (_, _) => RefreshRows();
+ DockPanel.SetDock(_search, Dock.Left);
+ top.Children.Add(_search);
+ Grid.SetRow(top, 0);
+ root.Children.Add(top);
+
+ _grid.AutoGenerateColumns = false;
+ _grid.CanUserAddRows = false;
+ _grid.CanUserDeleteRows = false;
+ _grid.SelectionMode = DataGridSelectionMode.Single;
+ _grid.SelectionUnit = DataGridSelectionUnit.FullRow;
+ _grid.Columns.Add(new DataGridCheckBoxColumn
+ {
+ Header = "✓",
+ Binding = new Binding(nameof(RemovedSignalRow.IsSelected)) { Mode = BindingMode.TwoWay },
+ Width = 42
+ });
+ _grid.Columns.Add(Column("IED", nameof(RemovedSignalRow.IedName), 120));
+ _grid.Columns.Add(Column("DataSet", nameof(RemovedSignalRow.DataSetReference), 210));
+ _grid.Columns.Add(Column("#", nameof(RemovedSignalRow.DataSetMemberIndex), 45));
+ _grid.Columns.Add(Column("Signal", nameof(RemovedSignalRow.StaticMemberReference), 360));
+ _grid.Columns.Add(Column("Kind", nameof(RemovedSignalRow.SignalKind), 80));
+ var restoreOne = new MenuItem { Header = "Restore to FAT" };
+ restoreOne.Click += RestoreOne_Click;
+ _grid.ContextMenu = new ContextMenu { Items = { restoreOne } };
+ Grid.SetRow(_grid, 1);
+ root.Children.Add(_grid);
+
+ var actions = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ HorizontalAlignment = HorizontalAlignment.Right,
+ Margin = new Thickness(0, 12, 0, 0)
+ };
+ actions.Children.Add(Button("Select All", (_, _) => SetVisibleSelection(true)));
+ actions.Children.Add(Button("Deselect All", (_, _) => SetVisibleSelection(false)));
+ actions.Children.Add(Button("Restore Selected", RestoreSelected_Click, true));
+ actions.Children.Add(Button("Cancel", (_, _) => Close()));
+ Grid.SetRow(actions, 2);
+ root.Children.Add(actions);
+ return root;
+ }
+
+ private static DataGridTextColumn Column(string header, string path, double width)
+ => new() { Header = header, Binding = new Binding(path), Width = new DataGridLength(width) };
+
+ private static Button Button(string text, RoutedEventHandler click, bool primary = false)
+ {
+ var button = new Button
+ {
+ Content = text,
+ MinWidth = 100,
+ Height = 32,
+ Padding = new Thickness(10, 0, 10, 0),
+ Margin = new Thickness(8, 0, 0, 0),
+ FontWeight = primary ? FontWeights.SemiBold : FontWeights.Normal
+ };
+ button.Click += click;
+ return button;
+ }
+
+ private IEnumerable VisibleRows()
+ {
+ var query = _search.Text.Trim();
+ return query.Length == 0
+ ? _rows
+ : _rows.Where(row =>
+ Contains(row.IedName, query) ||
+ Contains(row.DataSetReference, query) ||
+ Contains(row.StaticMemberReference, query) ||
+ Contains(row.SignalKind.ToString(), query));
+ }
+
+ private void RefreshRows() => _grid.ItemsSource = VisibleRows().ToArray();
+
+ private void SetVisibleSelection(bool selected)
+ {
+ foreach (var row in VisibleRows())
+ row.IsSelected = selected;
+ _grid.Items.Refresh();
+ }
+
+ private void RestoreSelected_Click(object sender, RoutedEventArgs e)
+ {
+ var ids = _rows.Where(row => row.IsSelected).Select(row => row.SignalId).ToArray();
+ if (ids.Length == 0)
+ return;
+ _project.RestoreSignals(ids);
+ DialogResult = true;
+ }
+
+ private void RestoreOne_Click(object sender, RoutedEventArgs e)
+ {
+ if (_grid.SelectedItem is not RemovedSignalRow row)
+ return;
+ _project.RestoreSignal(row.SignalId);
+ _rows.Remove(row);
+ RefreshRows();
+ }
+
+ private static bool Contains(string? value, string query)
+ => (value ?? string.Empty).Contains(query, StringComparison.OrdinalIgnoreCase);
+}
diff --git a/MainWindow.FatV2.cs b/MainWindow.FatV2.cs
new file mode 100644
index 000000000..22f346faa
--- /dev/null
+++ b/MainWindow.FatV2.cs
@@ -0,0 +1,228 @@
+using System.Globalization;
+using System.IO;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Threading;
+using ArIED61850Tester.Models;
+using ArIED61850Tester.Models.IoTesting;
+using ArIED61850Tester.Services.IoTesting;
+using Microsoft.Win32;
+
+namespace ArIED61850Tester;
+
+public partial class MainWindow
+{
+ private FatVerificationWindow? _fatV2Window;
+
+ private void ScheduleFatV2LauncherInstall()
+ {
+ // P4 remains additive to the proven workbook FAT path. Install the SCL FAT command
+ // after the existing first-run card has been constructed instead of rewriting the
+ // legacy launcher or its session lifecycle.
+ Dispatcher.BeginInvoke(
+ new Action(InstallFatV2Launcher),
+ DispatcherPriority.Background);
+ }
+
+ private void InstallFatV2Launcher()
+ {
+ if (_ioListTestingLauncherCard is not Border { Child: StackPanel content })
+ return;
+ if (content.Children.OfType