diff --git a/IoListTestingWindow.EmbeddedEngineeringHost.cs b/IoListTestingWindow.EmbeddedEngineeringHost.cs
new file mode 100644
index 000000000..5e434facc
--- /dev/null
+++ b/IoListTestingWindow.EmbeddedEngineeringHost.cs
@@ -0,0 +1,240 @@
+using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Threading;
+using ArIED61850Tester.Models;
+
+namespace ArIED61850Tester;
+
+///
+/// Hosts the proven IoListTestingWindow production workspace inside MainWindow's FAT tab.
+/// The production Window remains loaded-but-hidden so its existing session controller,
+/// auto-capture lifecycle, persistence and report-preview code stay the single FAT authority.
+/// Only its central workspace + FAT status footer are re-parented into Engineering.
+///
+public partial class IoListTestingWindow
+{
+ private bool _engineeringEmbeddedMountQueued;
+ private bool _engineeringEmbeddedMounted;
+ private FrameworkElement? _engineeringEmbeddedSurface;
+
+ [ModuleInitializer]
+ internal static void RegisterEmbeddedEngineeringFatHost()
+ {
+ EventManager.RegisterClassHandler(
+ typeof(IoListTestingWindow),
+ FrameworkElement.LoadedEvent,
+ new RoutedEventHandler(EmbeddedEngineeringFatHost_Loaded),
+ handledEventsToo: true);
+ }
+
+ private static void EmbeddedEngineeringFatHost_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is not IoListTestingWindow window ||
+ !ReferenceEquals(e.OriginalSource, window) ||
+ window._engineeringEmbeddedMounted ||
+ window._engineeringEmbeddedMountQueued ||
+ window.Owner is not MainWindow owner ||
+ !owner.ProductionFatTabReady)
+ {
+ return;
+ }
+
+ window._engineeringEmbeddedMountQueued = true;
+
+ // MainWindow's legacy launcher still calls Show() on this Window. Make that bootstrap
+ // surface invisible immediately; the actual production visual is moved into Engineering
+ // on the next Loaded-priority dispatcher turn, before ContextIdle command-panel work.
+ window.ShowActivated = false;
+ window.ShowInTaskbar = false;
+ window.Opacity = 0d;
+
+ window.Dispatcher.BeginInvoke(
+ DispatcherPriority.Loaded,
+ new Action(() => window.TryMountIntoEngineering(owner)));
+ }
+
+ private void TryMountIntoEngineering(MainWindow owner)
+ {
+ _engineeringEmbeddedMountQueued = false;
+ if (_engineeringEmbeddedMounted || !IsLoaded || !ReferenceEquals(Owner, owner) || !owner.ProductionFatTabReady)
+ return;
+
+ try
+ {
+ EnsureProductionFatPresentationForEmbeddedHost();
+ DisableLegacyEmbeddedCommandPanel();
+ var surface = DetachProductionFatCentralWorkspace();
+ if (surface == null)
+ return;
+
+ _engineeringEmbeddedSurface = surface;
+ _engineeringEmbeddedMounted = owner.MountProductionFatWorkspace(this, surface);
+ if (!_engineeringEmbeddedMounted)
+ return;
+
+ // The central FAT view now belongs to MainWindow. Keep this Window loaded and
+ // hidden because existing controller/session/event code is intentionally reused.
+ Hide();
+ }
+ catch (Exception ex)
+ {
+ Opacity = 1d;
+ ShowInTaskbar = true;
+ ShowActivated = true;
+ MessageBox.Show(
+ owner,
+ $"ARSAS could not embed the production FAT workspace. The standalone FAT window will remain available.\n\n{ex.Message}",
+ "FAT workspace host",
+ MessageBoxButton.OK,
+ MessageBoxImage.Warning);
+ }
+ }
+
+ private void EnsureProductionFatPresentationForEmbeddedHost()
+ {
+ // P1 normally installs this during Loaded. Calling it explicitly is safe/idempotent
+ // and guarantees the first embedded frame is the same V2 FAT grid as the old Window.
+ InstallFatV2WorkspaceUx();
+
+ // PrintPreview historically installs from OnContentRendered. The bootstrap Window is
+ // intentionally transparent, so install the exact same production preview explicitly
+ // before the central workspace is detached. This preserves full-center mode switching.
+ if (!_printPreviewInstalled)
+ {
+ InstallPerIedPrintPreview();
+ PropertyChanged += PrintPreviewWindow_PropertyChanged;
+ Session.PropertyChanged += PrintPreviewSession_PropertyChanged;
+ Closed += PrintPreviewWindow_Closed;
+ _printPreviewInstalled = true;
+ }
+
+ // The embedded center already declares WorkspacePreviewToggle in XAML. Make that
+ // visible button the production toggle authority instead of the hidden Window header.
+ if (WorkspacePreviewToggle != null)
+ _printPreviewToggle = WorkspacePreviewToggle;
+ }
+
+ private void DisableLegacyEmbeddedCommandPanel()
+ {
+ // Engineering already owns one shared Command Dock. Prevent the old FAT window's
+ // duplicate command panel from being created/refreshed while embedded. A non-null
+ // sentinel makes the queued legacy installer return immediately; null row/summary
+ // references make its queued refresh a no-op. No command runtime semantics change.
+ DetachFatCommandDevice();
+ if (_fatCommandPanelShell?.Parent is Grid existingHost)
+ {
+ var row = Grid.GetRow(_fatCommandPanelShell);
+ existingHost.Children.Remove(_fatCommandPanelShell);
+ if (row >= 0 && row < existingHost.RowDefinitions.Count)
+ existingHost.RowDefinitions[row].Height = new GridLength(0);
+ if (row - 1 >= 0 && row - 1 < existingHost.RowDefinitions.Count)
+ existingHost.RowDefinitions[row - 1].Height = new GridLength(0);
+ }
+
+ _fatCommandPanelShell ??= new Border { Visibility = Visibility.Collapsed };
+ _fatCommandRows = null;
+ _fatCommandSummary = null;
+ _fatCommandEmptyState = null;
+ }
+
+ private FrameworkElement? DetachProductionFatCentralWorkspace()
+ {
+ if (Content is not Grid root)
+ return null;
+
+ var middle = root.Children
+ .OfType()
+ .FirstOrDefault(child => Grid.GetRow(child) == 2);
+ var workspaceBorder = middle?.Children
+ .OfType()
+ .FirstOrDefault(child => Grid.GetColumn(child) == 2);
+ if (middle == null || workspaceBorder == null)
+ return null;
+
+ var footer = root.Children
+ .OfType()
+ .FirstOrDefault(child => Grid.GetRow(child) == 4);
+
+ middle.Children.Remove(workspaceBorder);
+ if (footer != null)
+ root.Children.Remove(footer);
+
+ var host = new Grid
+ {
+ DataContext = this,
+ Margin = new Thickness(0)
+ };
+ host.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
+ if (footer != null)
+ {
+ host.RowDefinitions.Add(new RowDefinition { Height = new GridLength(8) });
+ host.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ }
+
+ workspaceBorder.Margin = new Thickness(0);
+ Grid.SetRow(workspaceBorder, 0);
+ Grid.SetColumn(workspaceBorder, 0);
+ host.Children.Add(workspaceBorder);
+
+ if (footer != null)
+ {
+ footer.Margin = new Thickness(0);
+ Grid.SetRow(footer, 2);
+ Grid.SetColumn(footer, 0);
+ host.Children.Add(footer);
+ }
+
+ return host;
+ }
+
+ internal void SelectEngineeringDeviceForEmbeddedFat(Iec61850MonitorDevice? device)
+ {
+ if (!_engineeringEmbeddedMounted || device == null)
+ return;
+
+ var match = Project.Ieds.FirstOrDefault(ied =>
+ (!string.IsNullOrWhiteSpace(ied.LiveDeviceId) &&
+ ied.LiveDeviceId.Equals(device.DeviceId, StringComparison.OrdinalIgnoreCase)) ||
+ (!string.IsNullOrWhiteSpace(ied.IpAddress) &&
+ ied.IpAddress.Equals(device.IpAddress, StringComparison.OrdinalIgnoreCase)) ||
+ ied.IedName.Equals(device.SclIedName, StringComparison.OrdinalIgnoreCase) ||
+ ied.IedName.Equals(device.Name, StringComparison.OrdinalIgnoreCase));
+ if (match == null || ReferenceEquals(SelectedIed, match))
+ return;
+
+ // Do not retarget an active production FAT transaction/session. When idle, the
+ // persistent Engineering IED Explorer is the navigation/selection authority.
+ if (!CanSelectIed)
+ return;
+
+ SelectedIed = match;
+ }
+
+ internal void NotifyEmbeddedHostActivated()
+ {
+ if (!_engineeringEmbeddedMounted)
+ return;
+
+ RefreshFatV2WorkspaceUx(refreshRows: true);
+ if (_printPreviewActive)
+ RefreshPrintPreview();
+ }
+
+ private void EmbeddedEngineeringFatHost_Closed(object? sender, EventArgs e)
+ {
+ if (Owner is MainWindow owner)
+ owner.UnmountProductionFatWorkspace(this);
+ Closed -= EmbeddedEngineeringFatHost_Closed;
+ _engineeringEmbeddedMounted = false;
+ _engineeringEmbeddedSurface = null;
+ }
+
+ // Field initializer cannot attach an instance event. Hook cleanup once the embedded
+ // surface has actually been mounted; this helper is called from the production owner.
+ internal void RegisterEmbeddedHostCloseCleanup()
+ {
+ Closed -= EmbeddedEngineeringFatHost_Closed;
+ Closed += EmbeddedEngineeringFatHost_Closed;
+ }
+}
diff --git a/MainWindow.NativeFatExplorerSync.cs b/MainWindow.NativeFatExplorerSync.cs
new file mode 100644
index 000000000..8ea74646d
--- /dev/null
+++ b/MainWindow.NativeFatExplorerSync.cs
@@ -0,0 +1,213 @@
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Threading;
+using ArIED61850Tester.Models;
+
+namespace ArIED61850Tester;
+
+///
+/// Keeps the native FAT projection aligned with the persistent IED Explorer's signal
+/// selection without turning FAT into a second signal database.
+///
+/// ObservableCollection changes were already reconciled by the first P2 slice. Explorer
+/// checkbox changes are different: SignalDefinition stays in the collection and only
+/// IsSelected changes. This bridge observes that property, marks the FAT scope dirty, and
+/// forces non-destructive reconciliation when FAT is visible (or the next time it opens).
+///
+public partial class MainWindow
+{
+ private DispatcherTimer? _nativeFatExplorerSyncInstallRetry;
+ private DispatcherTimer? _nativeFatExplorerScopeTimer;
+ private Iec61850MonitorDevice? _nativeFatExplorerSyncDevice;
+ private readonly HashSet _nativeFatExplorerObservedSignals = new();
+ private bool _nativeFatExplorerSyncAttached;
+ private bool _nativeFatExplorerScopeDirty;
+
+ [ModuleInitializer]
+ internal static void RegisterNativeFatExplorerSelectionSync()
+ {
+ EventManager.RegisterClassHandler(
+ typeof(MainWindow),
+ FrameworkElement.LoadedEvent,
+ new RoutedEventHandler(NativeFatExplorerSync_MainWindowLoaded),
+ handledEventsToo: true);
+ }
+
+ private static void NativeFatExplorerSync_MainWindowLoaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is not MainWindow window || window._nativeFatExplorerSyncAttached)
+ return;
+
+ window.Dispatcher.BeginInvoke(
+ DispatcherPriority.ApplicationIdle,
+ new Action(window.TryAttachNativeFatExplorerSelectionSync));
+ }
+
+ private void TryAttachNativeFatExplorerSelectionSync()
+ {
+ if (_nativeFatExplorerSyncAttached || !IsLoaded)
+ return;
+
+ if (!_nativeFatInstalled)
+ {
+ _nativeFatExplorerSyncInstallRetry ??= new DispatcherTimer(DispatcherPriority.ApplicationIdle)
+ {
+ Interval = TimeSpan.FromMilliseconds(180)
+ };
+ _nativeFatExplorerSyncInstallRetry.Tick -= NativeFatExplorerSyncInstallRetry_Tick;
+ _nativeFatExplorerSyncInstallRetry.Tick += NativeFatExplorerSyncInstallRetry_Tick;
+ _nativeFatExplorerSyncInstallRetry.Start();
+ return;
+ }
+
+ _nativeFatExplorerSyncInstallRetry?.Stop();
+ _nativeFatExplorerSyncAttached = true;
+
+ _nativeFatExplorerScopeTimer = new DispatcherTimer(DispatcherPriority.Background)
+ {
+ Interval = TimeSpan.FromMilliseconds(180)
+ };
+ _nativeFatExplorerScopeTimer.Tick += NativeFatExplorerScopeTimer_Tick;
+
+ PropertyChanged += NativeFatExplorerSync_MainWindowPropertyChanged;
+ MainTabs.SelectionChanged += NativeFatExplorerSync_MainTabsSelectionChanged;
+ Closed += NativeFatExplorerSync_MainWindowClosed;
+ RebindNativeFatExplorerSignals(SelectedDevice);
+ }
+
+ private void NativeFatExplorerSyncInstallRetry_Tick(object? sender, EventArgs e)
+ {
+ _nativeFatExplorerSyncInstallRetry?.Stop();
+ TryAttachNativeFatExplorerSelectionSync();
+ }
+
+ private void NativeFatExplorerSync_MainWindowPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName != nameof(SelectedDevice))
+ return;
+
+ RebindNativeFatExplorerSignals(SelectedDevice);
+ MarkNativeFatExplorerScopeDirty();
+ }
+
+ private void RebindNativeFatExplorerSignals(Iec61850MonitorDevice? device)
+ {
+ if (ReferenceEquals(_nativeFatExplorerSyncDevice, device))
+ {
+ SyncNativeFatExplorerSignalSubscriptions();
+ return;
+ }
+
+ if (_nativeFatExplorerSyncDevice != null)
+ _nativeFatExplorerSyncDevice.Signals.CollectionChanged -= NativeFatExplorerSignals_CollectionChanged;
+
+ foreach (var signal in _nativeFatExplorerObservedSignals)
+ signal.PropertyChanged -= NativeFatExplorerSignal_PropertyChanged;
+ _nativeFatExplorerObservedSignals.Clear();
+
+ _nativeFatExplorerSyncDevice = device;
+ if (_nativeFatExplorerSyncDevice != null)
+ {
+ _nativeFatExplorerSyncDevice.Signals.CollectionChanged += NativeFatExplorerSignals_CollectionChanged;
+ SyncNativeFatExplorerSignalSubscriptions();
+ }
+ }
+
+ private void NativeFatExplorerSignals_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ SyncNativeFatExplorerSignalSubscriptions();
+ MarkNativeFatExplorerScopeDirty();
+ }
+
+ private void SyncNativeFatExplorerSignalSubscriptions()
+ {
+ var current = _nativeFatExplorerSyncDevice?.Signals.ToHashSet() ?? new HashSet();
+
+ foreach (var signal in _nativeFatExplorerObservedSignals.Where(signal => !current.Contains(signal)).ToArray())
+ {
+ signal.PropertyChanged -= NativeFatExplorerSignal_PropertyChanged;
+ _nativeFatExplorerObservedSignals.Remove(signal);
+ }
+
+ foreach (var signal in current)
+ {
+ if (!_nativeFatExplorerObservedSignals.Add(signal))
+ continue;
+ signal.PropertyChanged += NativeFatExplorerSignal_PropertyChanged;
+ }
+ }
+
+ private void NativeFatExplorerSignal_PropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ // IsSelected is the Explorer membership authority. Value/quality/timestamp changes
+ // stay live-bound directly and must never trigger a full FAT reconciliation.
+ if (e.PropertyName == nameof(SignalDefinition.IsSelected))
+ MarkNativeFatExplorerScopeDirty();
+ }
+
+ private void MarkNativeFatExplorerScopeDirty()
+ {
+ _nativeFatExplorerScopeDirty = true;
+ if (MainTabs.SelectedIndex != NativeFatWorkspaceIndex)
+ return;
+
+ _nativeFatExplorerScopeTimer?.Stop();
+ _nativeFatExplorerScopeTimer?.Start();
+ }
+
+ private async void NativeFatExplorerScopeTimer_Tick(object? sender, EventArgs e)
+ {
+ _nativeFatExplorerScopeTimer?.Stop();
+ if (!_nativeFatExplorerScopeDirty || MainTabs.SelectedIndex != NativeFatWorkspaceIndex)
+ return;
+
+ await EnsureNativeFatLoadedAsync(forceReconcile: true);
+ _nativeFatExplorerScopeDirty = false;
+ }
+
+ private void NativeFatExplorerSync_MainTabsSelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (!ReferenceEquals(e.Source, MainTabs) ||
+ MainTabs.SelectedIndex != NativeFatWorkspaceIndex ||
+ !_nativeFatExplorerScopeDirty)
+ {
+ return;
+ }
+
+ // The base P2 selection handler may have issued a fast non-forced load already.
+ // Re-run at ContextIdle with forceReconcile so checkbox changes made while another
+ // workspace was active are never hidden behind the current-state fast path.
+ Dispatcher.BeginInvoke(
+ DispatcherPriority.ContextIdle,
+ new Action(async () =>
+ {
+ if (MainTabs.SelectedIndex != NativeFatWorkspaceIndex || !_nativeFatExplorerScopeDirty)
+ return;
+ await EnsureNativeFatLoadedAsync(forceReconcile: true);
+ _nativeFatExplorerScopeDirty = false;
+ }));
+ }
+
+ private void NativeFatExplorerSync_MainWindowClosed(object? sender, EventArgs e)
+ {
+ _nativeFatExplorerSyncInstallRetry?.Stop();
+ _nativeFatExplorerScopeTimer?.Stop();
+
+ PropertyChanged -= NativeFatExplorerSync_MainWindowPropertyChanged;
+ MainTabs.SelectionChanged -= NativeFatExplorerSync_MainTabsSelectionChanged;
+ Closed -= NativeFatExplorerSync_MainWindowClosed;
+
+ if (_nativeFatExplorerSyncDevice != null)
+ _nativeFatExplorerSyncDevice.Signals.CollectionChanged -= NativeFatExplorerSignals_CollectionChanged;
+ foreach (var signal in _nativeFatExplorerObservedSignals)
+ signal.PropertyChanged -= NativeFatExplorerSignal_PropertyChanged;
+
+ _nativeFatExplorerObservedSignals.Clear();
+ _nativeFatExplorerSyncDevice = null;
+ _nativeFatExplorerSyncAttached = false;
+ _nativeFatExplorerScopeDirty = false;
+ }
+}
diff --git a/MainWindow.NativeFatExport.cs b/MainWindow.NativeFatExport.cs
new file mode 100644
index 000000000..426cd18c8
--- /dev/null
+++ b/MainWindow.NativeFatExport.cs
@@ -0,0 +1,166 @@
+using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Threading;
+using ArIED61850Tester.Services;
+using Microsoft.Win32;
+
+namespace ArIED61850Tester;
+
+///
+/// Native evidence export entry point. Export consumes the immutable report snapshot
+/// created from the already-loaded FAT state; it never creates a second IED session.
+///
+public partial class MainWindow
+{
+ private DispatcherTimer? _nativeFatExportInstallRetry;
+ private Button? _nativeFatExportPdfButton;
+ private bool _nativeFatExportInstalled;
+
+ [ModuleInitializer]
+ internal static void RegisterNativeFatExport()
+ {
+ EventManager.RegisterClassHandler(
+ typeof(MainWindow),
+ FrameworkElement.LoadedEvent,
+ new RoutedEventHandler(NativeFatExport_MainWindowLoaded),
+ handledEventsToo: true);
+ }
+
+ private static void NativeFatExport_MainWindowLoaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is not MainWindow window || window._nativeFatExportInstalled)
+ return;
+
+ window.Dispatcher.BeginInvoke(
+ DispatcherPriority.ApplicationIdle,
+ new Action(window.TryInstallNativeFatExport));
+ }
+
+ private void TryInstallNativeFatExport()
+ {
+ if (_nativeFatExportInstalled || !IsLoaded)
+ return;
+
+ if (!_nativeFatInstalled || _nativeFatSearchBox?.Parent is not WrapPanel toolbar)
+ {
+ _nativeFatExportInstallRetry ??= new DispatcherTimer(DispatcherPriority.ApplicationIdle)
+ {
+ Interval = TimeSpan.FromMilliseconds(190)
+ };
+ _nativeFatExportInstallRetry.Tick -= NativeFatExportInstallRetry_Tick;
+ _nativeFatExportInstallRetry.Tick += NativeFatExportInstallRetry_Tick;
+ _nativeFatExportInstallRetry.Start();
+ return;
+ }
+
+ _nativeFatExportInstallRetry?.Stop();
+ _nativeFatExportInstalled = true;
+ _nativeFatExportPdfButton = CreateNativeFatButton(
+ "Export PDF",
+ NativeFatExportPdf_Click,
+ "Export an immutable native FAT evidence PDF for the selected IED. Show historical controls whether historical signal rows are included.");
+
+ var historyIndex = _nativeFatShowHistoricalCheck == null
+ ? toolbar.Children.Count
+ : toolbar.Children.IndexOf(_nativeFatShowHistoricalCheck);
+ if (historyIndex < 0)
+ historyIndex = toolbar.Children.Count;
+ toolbar.Children.Insert(historyIndex, _nativeFatExportPdfButton);
+
+ Closed += NativeFatExport_MainWindowClosed;
+ }
+
+ private void NativeFatExportInstallRetry_Tick(object? sender, EventArgs e)
+ {
+ _nativeFatExportInstallRetry?.Stop();
+ TryInstallNativeFatExport();
+ }
+
+ private async void NativeFatExportPdf_Click(object sender, RoutedEventArgs e)
+ {
+ if (_nativeFatExportPdfButton == null)
+ return;
+
+ await EnsureNativeFatLoadedAsync(forceReconcile: false);
+ var device = SelectedDevice;
+ var state = _nativeFatCurrentState;
+ if (device == null || state == null ||
+ !state.DeviceId.Equals(device.DeviceId, StringComparison.OrdinalIgnoreCase))
+ {
+ SetNativeFatStatus("Select an IED with a loaded FAT state before exporting.");
+ return;
+ }
+
+ var includeHistorical = _nativeFatShowHistoricalCheck?.IsChecked == true;
+ var snapshot = NativeFatReportSnapshotBuilder.Build(device, state);
+ var dialog = new SaveFileDialog
+ {
+ Title = "Export ARSAS native FAT evidence PDF",
+ Filter = "PDF evidence report (*.pdf)|*.pdf",
+ FileName = $"{SafeNativeFatFileName(device.Name)}_FAT_{DateTime.Now:yyyyMMdd_HHmm}.pdf",
+ AddExtension = true,
+ DefaultExt = ".pdf",
+ OverwritePrompt = true
+ };
+ if (dialog.ShowDialog(this) != true)
+ return;
+
+ var previousContent = _nativeFatExportPdfButton.Content;
+ try
+ {
+ _nativeFatExportPdfButton.IsEnabled = false;
+ _nativeFatExportPdfButton.Content = "Exporting…";
+ SetNativeFatStatus("Saving current FAT state and building evidence PDF…");
+
+ // Persist the exact state used to construct the snapshot before delivering
+ // an external evidence file. The snapshot itself remains frozen thereafter.
+ await SaveNativeFatStateAsync(state);
+ await Task.Run(() => NativeFatPdfReportService.Save(
+ dialog.FileName,
+ snapshot,
+ includeHistorical));
+
+ SetNativeFatStatus($"PDF exported · {Path.GetFileName(dialog.FileName)}");
+ AddLog(
+ "INFO",
+ "Native FAT",
+ $"Native FAT PDF exported for {device.Name}: {dialog.FileName}" +
+ (includeHistorical ? " (historical included)" : string.Empty));
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException or InvalidOperationException or ArgumentException)
+ {
+ AddLog("WARN", "Native FAT", $"PDF export failed: {ex.Message}");
+ SetNativeFatStatus("PDF export failed. Native FAT state remains saved.");
+ MessageBox.Show(
+ this,
+ ex.Message,
+ "Native FAT PDF export failed",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error);
+ }
+ finally
+ {
+ _nativeFatExportPdfButton.Content = previousContent ?? "Export PDF";
+ _nativeFatExportPdfButton.IsEnabled = true;
+ }
+ }
+
+ private static string SafeNativeFatFileName(string? value)
+ {
+ var source = string.IsNullOrWhiteSpace(value) ? "IED" : value.Trim();
+ var invalid = Path.GetInvalidFileNameChars().ToHashSet();
+ var result = new string(source.Select(character => invalid.Contains(character) ? '_' : character).ToArray())
+ .Trim()
+ .Trim('.');
+ return string.IsNullOrWhiteSpace(result) ? "IED" : result;
+ }
+
+ private void NativeFatExport_MainWindowClosed(object? sender, EventArgs e)
+ {
+ _nativeFatExportInstallRetry?.Stop();
+ if (_nativeFatExportPdfButton != null)
+ _nativeFatExportPdfButton.Click -= NativeFatExportPdf_Click;
+ Closed -= NativeFatExport_MainWindowClosed;
+ }
+}
diff --git a/MainWindow.NativeFatHistoryInspector.cs b/MainWindow.NativeFatHistoryInspector.cs
new file mode 100644
index 000000000..27b24d82c
--- /dev/null
+++ b/MainWindow.NativeFatHistoryInspector.cs
@@ -0,0 +1,178 @@
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Threading;
+using ArIED61850Tester.Models;
+
+namespace ArIED61850Tester;
+
+///
+/// Makes the native FAT report preview useful as an operator evidence/history inspector.
+/// The immutable report snapshot remains the preview/export authority; this enhancement
+/// only appends the persisted chronological audit trail for the selected IEC identity.
+///
+public partial class MainWindow
+{
+ private DispatcherTimer? _nativeFatHistoryInspectorInstallRetry;
+ private ScrollViewer? _nativeFatHistoryScrollViewer;
+ private bool _nativeFatHistoryInspectorAttached;
+ private bool _nativeFatHistoryRenderQueued;
+
+ [ModuleInitializer]
+ internal static void RegisterNativeFatHistoryInspector()
+ {
+ EventManager.RegisterClassHandler(
+ typeof(MainWindow),
+ FrameworkElement.LoadedEvent,
+ new RoutedEventHandler(NativeFatHistoryInspector_MainWindowLoaded),
+ handledEventsToo: true);
+ }
+
+ private static void NativeFatHistoryInspector_MainWindowLoaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is not MainWindow window || window._nativeFatHistoryInspectorAttached)
+ return;
+
+ window.Dispatcher.BeginInvoke(
+ DispatcherPriority.ApplicationIdle,
+ new Action(window.TryAttachNativeFatHistoryInspector));
+ }
+
+ private void TryAttachNativeFatHistoryInspector()
+ {
+ if (_nativeFatHistoryInspectorAttached || !IsLoaded)
+ return;
+
+ if (!_nativeFatReportPreviewEnhanced ||
+ _nativeFatPreviewPane?.Child is not Grid previewRoot ||
+ _nativeFatPreviewGrid == null ||
+ _nativeFatPreviewEvidenceText == null)
+ {
+ _nativeFatHistoryInspectorInstallRetry ??= new DispatcherTimer(DispatcherPriority.ApplicationIdle)
+ {
+ Interval = TimeSpan.FromMilliseconds(180)
+ };
+ _nativeFatHistoryInspectorInstallRetry.Tick -= NativeFatHistoryInspectorInstallRetry_Tick;
+ _nativeFatHistoryInspectorInstallRetry.Tick += NativeFatHistoryInspectorInstallRetry_Tick;
+ _nativeFatHistoryInspectorInstallRetry.Start();
+ return;
+ }
+
+ _nativeFatHistoryInspectorInstallRetry?.Stop();
+ _nativeFatHistoryInspectorAttached = true;
+
+ // The evidence block can become long once retest history exists. Keep the report
+ // pane compact and give only the evidence/history area its own vertical scrolling.
+ if (previewRoot.Children.Contains(_nativeFatPreviewEvidenceText))
+ previewRoot.Children.Remove(_nativeFatPreviewEvidenceText);
+
+ _nativeFatPreviewEvidenceText.Margin = new Thickness(0);
+ _nativeFatPreviewEvidenceText.TextWrapping = TextWrapping.Wrap;
+ _nativeFatHistoryScrollViewer = new ScrollViewer
+ {
+ Content = _nativeFatPreviewEvidenceText,
+ Margin = new Thickness(0, 8, 0, 0),
+ MaxHeight = 190,
+ VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
+ HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
+ CanContentScroll = false
+ };
+ Grid.SetRow(_nativeFatHistoryScrollViewer, 5);
+ previewRoot.Children.Add(_nativeFatHistoryScrollViewer);
+
+ _nativeFatPreviewGrid.SelectionChanged += NativeFatHistoryInspector_SelectionChanged;
+ Closed += NativeFatHistoryInspector_MainWindowClosed;
+ QueueNativeFatHistoryInspectorRender();
+ }
+
+ private void NativeFatHistoryInspectorInstallRetry_Tick(object? sender, EventArgs e)
+ {
+ _nativeFatHistoryInspectorInstallRetry?.Stop();
+ TryAttachNativeFatHistoryInspector();
+ }
+
+ private void NativeFatHistoryInspector_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ => QueueNativeFatHistoryInspectorRender();
+
+ private void QueueNativeFatHistoryInspectorRender()
+ {
+ if (!_nativeFatHistoryInspectorAttached || _nativeFatHistoryRenderQueued)
+ return;
+
+ _nativeFatHistoryRenderQueued = true;
+ Dispatcher.BeginInvoke(
+ DispatcherPriority.ContextIdle,
+ new Action(() =>
+ {
+ _nativeFatHistoryRenderQueued = false;
+ RenderNativeFatHistoryInspector();
+ }));
+ }
+
+ private void RenderNativeFatHistoryInspector()
+ {
+ if (_nativeFatPreviewEvidenceText == null)
+ return;
+
+ if (_nativeFatPreviewGrid?.SelectedItem is not NativeFatReportRow reportRow)
+ {
+ _nativeFatPreviewEvidenceText.Text = "Select a report row to inspect captured evidence and retest history.";
+ return;
+ }
+
+ var text = new StringBuilder(reportRow.EvidenceSummaryText);
+ var state = _nativeFatCurrentState;
+ var key = NativeFatIdentity.BuildKey(reportRow.IecReference, reportRow.FunctionalConstraint);
+ var persisted = state?.Signals.FirstOrDefault(signal =>
+ signal.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
+ var history = persisted?.History ?? new List();
+
+ text.Append("\n\nHISTORY · latest first");
+ if (history.Count == 0)
+ {
+ text.Append("\nNo previous capture/result transitions recorded.");
+ }
+ else
+ {
+ foreach (var entry in history
+ .OrderByDescending(item => item.TimestampUtc)
+ .Take(8))
+ {
+ text.Append("\n")
+ .Append(entry.TimestampUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"))
+ .Append(" · ")
+ .Append(string.IsNullOrWhiteSpace(entry.Action) ? "State update" : entry.Action.Trim());
+
+ if (!string.IsNullOrWhiteSpace(entry.Result))
+ text.Append(" · ").Append(entry.Result.Trim());
+ if (entry.Value1 != null)
+ text.Append(" · V1=").Append(entry.Value1.Value);
+ if (entry.Value2 != null)
+ text.Append(" · V2=").Append(entry.Value2.Value);
+ if (!string.IsNullOrWhiteSpace(entry.Note))
+ text.Append(" · ").Append(entry.Note.Trim());
+ }
+
+ if (history.Count > 8)
+ text.Append("\n+").Append(history.Count - 8).Append(" earlier record(s) retained in the per-IED JSON.");
+ }
+
+ if (reportRow.IsHistorical)
+ text.Append("\n\nThis IEC identity is historical: it is no longer in the current Explorer scope, but its FAT evidence is retained.");
+
+ _nativeFatPreviewEvidenceText.Text = text.ToString();
+ _nativeFatHistoryScrollViewer?.ScrollToTop();
+ }
+
+ private void NativeFatHistoryInspector_MainWindowClosed(object? sender, EventArgs e)
+ {
+ _nativeFatHistoryInspectorInstallRetry?.Stop();
+ if (_nativeFatPreviewGrid != null)
+ _nativeFatPreviewGrid.SelectionChanged -= NativeFatHistoryInspector_SelectionChanged;
+ Closed -= NativeFatHistoryInspector_MainWindowClosed;
+ _nativeFatHistoryInspectorAttached = false;
+ _nativeFatHistoryRenderQueued = false;
+ _nativeFatHistoryScrollViewer = null;
+ }
+}
diff --git a/MainWindow.NativeFatPersistenceSafety.cs b/MainWindow.NativeFatPersistenceSafety.cs
new file mode 100644
index 000000000..4f7e192b2
--- /dev/null
+++ b/MainWindow.NativeFatPersistenceSafety.cs
@@ -0,0 +1,120 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Threading;
+
+namespace ArIED61850Tester;
+
+///
+/// Persistence durability guard for native FAT.
+///
+/// The normal 350 ms autosave debounce keeps multi-row operations cheap, but two short
+/// windows still need explicit protection:
+/// 1) the operator switches IED before the debounce fires; and
+/// 2) the operator closes ARSAS immediately after a capture/result change.
+///
+/// Native FAT caches one state object per stable IED. Flush inactive cached states after
+/// a SelectedDevice change and flush every cached state synchronously during Closing so
+/// evidence from the previously selected relay cannot be stranded in memory.
+///
+public partial class MainWindow
+{
+ private bool _nativeFatPersistenceSafetyAttached;
+ private bool _nativeFatSwitchFlushQueued;
+
+ [ModuleInitializer]
+ internal static void RegisterNativeFatPersistenceSafety()
+ {
+ EventManager.RegisterClassHandler(
+ typeof(MainWindow),
+ FrameworkElement.LoadedEvent,
+ new RoutedEventHandler(NativeFatPersistenceSafety_MainWindowLoaded),
+ handledEventsToo: true);
+ }
+
+ private static void NativeFatPersistenceSafety_MainWindowLoaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is not MainWindow window || window._nativeFatPersistenceSafetyAttached)
+ return;
+
+ window._nativeFatPersistenceSafetyAttached = true;
+ window.PropertyChanged += window.NativeFatPersistenceSafety_PropertyChanged;
+ window.Closing += window.NativeFatPersistenceSafety_Closing;
+ window.Closed += window.NativeFatPersistenceSafety_Closed;
+ }
+
+ private void NativeFatPersistenceSafety_PropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName != nameof(SelectedDevice) || !_nativeFatInstalled || _nativeFatSwitchFlushQueued)
+ return;
+
+ // Let the normal SelectedDevice handler finish rebinding the workspace first.
+ // The previous state remains in _nativeFatStateCache, so a ContextIdle flush can
+ // save it without blocking the IED switch or relying on _nativeFatCurrentState.
+ _nativeFatSwitchFlushQueued = true;
+ Dispatcher.BeginInvoke(
+ DispatcherPriority.ContextIdle,
+ new Action(() =>
+ {
+ _nativeFatSwitchFlushQueued = false;
+ _ = NativeFatPersistenceSafety_FlushInactiveStatesAsync();
+ }));
+ }
+
+ private async Task NativeFatPersistenceSafety_FlushInactiveStatesAsync()
+ {
+ if (_nativeFatStateCache.Count == 0)
+ return;
+
+ var current = _nativeFatCurrentState;
+ var inactive = _nativeFatStateCache.Values
+ .Where(state => !ReferenceEquals(state, current))
+ .Distinct()
+ .ToArray();
+
+ foreach (var state in inactive)
+ await SaveNativeFatStateAsync(state);
+ }
+
+ private void NativeFatPersistenceSafety_Closing(object? sender, CancelEventArgs e)
+ {
+ _nativeFatSaveTimer?.Stop();
+
+ var states = _nativeFatStateCache.Values.Distinct().ToList();
+ if (_nativeFatCurrentState != null && !states.Contains(_nativeFatCurrentState))
+ states.Add(_nativeFatCurrentState);
+ if (states.Count == 0)
+ return;
+
+ foreach (var state in states)
+ {
+ try
+ {
+ // Intentionally bypass the UI save gate here. A debounced asynchronous
+ // save may currently own that gate and need the dispatcher for its
+ // continuation; waiting for the gate synchronously could deadlock Closing.
+ // NativeFatStateStore uses unique temp files + atomic replace, so a
+ // concurrent same-state flush is safe and never truncates the valid file.
+ _nativeFatStore.SaveAsync(state, CancellationToken.None).GetAwaiter().GetResult();
+ }
+ catch (Exception ex)
+ {
+ // Shutdown must remain possible. Any previous valid JSON is preserved;
+ // record the specific IED failure while diagnostics are still available.
+ AddLog(
+ "WARN",
+ "Native FAT",
+ $"Final FAT persistence flush failed for {state.IedName}: {ex.Message}");
+ }
+ }
+ }
+
+ private void NativeFatPersistenceSafety_Closed(object? sender, EventArgs e)
+ {
+ PropertyChanged -= NativeFatPersistenceSafety_PropertyChanged;
+ Closing -= NativeFatPersistenceSafety_Closing;
+ Closed -= NativeFatPersistenceSafety_Closed;
+ _nativeFatPersistenceSafetyAttached = false;
+ _nativeFatSwitchFlushQueued = false;
+ }
+}
diff --git a/MainWindow.NativeFatReportPreview.cs b/MainWindow.NativeFatReportPreview.cs
new file mode 100644
index 000000000..bda0a5a72
--- /dev/null
+++ b/MainWindow.NativeFatReportPreview.cs
@@ -0,0 +1,279 @@
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Media;
+using System.Windows.Threading;
+using ArIED61850Tester.Models;
+using ArIED61850Tester.Services;
+
+namespace ArIED61850Tester;
+
+///
+/// P2 report-preview hardening for the native FAT workspace. The preview consumes an
+/// immutable evidence snapshot instead of the live row objects, while the main FAT grid
+/// remains directly bound to Explorer/runtime values for testing.
+///
+public partial class MainWindow
+{
+ private DispatcherTimer? _nativeFatReportPreviewInstallRetry;
+ private TextBlock? _nativeFatPreviewEvidenceText;
+ private NativeFatReportSnapshot? _nativeFatReportSnapshot;
+ private readonly HashSet _nativeFatReportObservedRows = new();
+ private bool _nativeFatReportPreviewEnhanced;
+ private bool _nativeFatReportRefreshQueued;
+
+ [ModuleInitializer]
+ internal static void RegisterNativeFatReportPreviewEnhancement()
+ {
+ EventManager.RegisterClassHandler(
+ typeof(MainWindow),
+ FrameworkElement.LoadedEvent,
+ new RoutedEventHandler(NativeFatReportPreview_MainWindowLoaded),
+ handledEventsToo: true);
+ }
+
+ private static void NativeFatReportPreview_MainWindowLoaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is not MainWindow window || window._nativeFatReportPreviewEnhanced)
+ return;
+
+ window.Dispatcher.BeginInvoke(
+ DispatcherPriority.ApplicationIdle,
+ new Action(window.TryInstallNativeFatReportPreviewEnhancement));
+ }
+
+ private void TryInstallNativeFatReportPreviewEnhancement()
+ {
+ if (_nativeFatReportPreviewEnhanced || !IsLoaded)
+ return;
+
+ if (!_nativeFatInstalled || _nativeFatPreviewPane?.Child is not Grid previewRoot ||
+ _nativeFatPreviewGrid == null || _nativeFatPreviewSummaryText == null)
+ {
+ _nativeFatReportPreviewInstallRetry ??= new DispatcherTimer(DispatcherPriority.ApplicationIdle)
+ {
+ Interval = TimeSpan.FromMilliseconds(180)
+ };
+ _nativeFatReportPreviewInstallRetry.Tick -= NativeFatReportPreviewInstallRetry_Tick;
+ _nativeFatReportPreviewInstallRetry.Tick += NativeFatReportPreviewInstallRetry_Tick;
+ _nativeFatReportPreviewInstallRetry.Start();
+ return;
+ }
+
+ _nativeFatReportPreviewInstallRetry?.Stop();
+ _nativeFatReportPreviewEnhanced = true;
+
+ // The initial P2 pane already has title/device/summary/grid/persistence rows. Add
+ // one compact evidence inspector between the grid and persistence note.
+ previewRoot.RowDefinitions.Insert(5, new RowDefinition { Height = GridLength.Auto });
+ if (_nativeFatPersistenceText != null)
+ Grid.SetRow(_nativeFatPersistenceText, 6);
+
+ _nativeFatPreviewEvidenceText = new TextBlock
+ {
+ Text = "Select a report row to inspect captured evidence.",
+ Margin = new Thickness(0, 8, 0, 0),
+ Padding = new Thickness(8, 7, 8, 7),
+ FontSize = 9.4,
+ Foreground = ResourceBrush("Muted", Color.FromRgb(0x66, 0x75, 0x8B)),
+ Background = new SolidColorBrush(Color.FromRgb(0xF7, 0xF9, 0xFC)),
+ TextWrapping = TextWrapping.Wrap
+ };
+ Grid.SetRow(_nativeFatPreviewEvidenceText, 5);
+ previewRoot.Children.Add(_nativeFatPreviewEvidenceText);
+
+ // Existing preview columns deliberately use property names also exposed by the
+ // snapshot row. Add the canonical IEC identity so report scope can be audited.
+ if (_nativeFatPreviewGrid.Columns.All(column => !Equals(column.Header, "IEC")))
+ {
+ _nativeFatPreviewGrid.Columns.Insert(1, new DataGridTextColumn
+ {
+ Header = "IEC",
+ Binding = new Binding(nameof(NativeFatReportRow.IecReference)) { Mode = BindingMode.OneWay },
+ Width = new DataGridLength(1.15, DataGridLengthUnitType.Star),
+ MinWidth = 105
+ });
+ }
+
+ if (_nativeFatPreviewGrid.Columns.Count >= 5)
+ {
+ _nativeFatPreviewGrid.Columns[0].Width = new DataGridLength(1.15, DataGridLengthUnitType.Star);
+ _nativeFatPreviewGrid.Columns[2].Width = new DataGridLength(0.58, DataGridLengthUnitType.Star);
+ _nativeFatPreviewGrid.Columns[3].Width = new DataGridLength(0.58, DataGridLengthUnitType.Star);
+ _nativeFatPreviewGrid.Columns[4].Width = new DataGridLength(0.62, DataGridLengthUnitType.Star);
+ }
+
+ _nativeFatPreviewGrid.SelectionChanged += NativeFatPreviewGrid_SelectionChanged;
+ _nativeFatPreviewPane!.IsVisibleChanged += NativeFatPreviewPane_IsVisibleChanged;
+ _nativeFatRows.CollectionChanged += NativeFatReportRows_CollectionChanged;
+ if (_nativeFatShowHistoricalCheck != null)
+ {
+ _nativeFatShowHistoricalCheck.Checked += NativeFatReportScope_Changed;
+ _nativeFatShowHistoricalCheck.Unchecked += NativeFatReportScope_Changed;
+ }
+
+ SyncNativeFatReportRowSubscriptions();
+ Closed += NativeFatReportPreview_MainWindowClosed;
+ QueueNativeFatReportPreviewRefresh();
+ }
+
+ private void NativeFatReportPreviewInstallRetry_Tick(object? sender, EventArgs e)
+ {
+ _nativeFatReportPreviewInstallRetry?.Stop();
+ TryInstallNativeFatReportPreviewEnhancement();
+ }
+
+ private void NativeFatPreviewPane_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
+ {
+ if (_nativeFatPreviewPane?.IsVisible == true && _nativeFatPreviewColumn != null)
+ {
+ // 330 px was enough for a four-column mock preview but not for auditable IEC
+ // identity + evidence. Keep it compact while making the report pane useful.
+ _nativeFatPreviewColumn.Width = new GridLength(430);
+ QueueNativeFatReportPreviewRefresh();
+ }
+ }
+
+ private void NativeFatReportRows_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ SyncNativeFatReportRowSubscriptions();
+ QueueNativeFatReportPreviewRefresh();
+ }
+
+ private void SyncNativeFatReportRowSubscriptions()
+ {
+ var current = _nativeFatRows.ToHashSet();
+ foreach (var row in _nativeFatReportObservedRows.Where(row => !current.Contains(row)).ToArray())
+ {
+ row.PropertyChanged -= NativeFatReportRow_PropertyChanged;
+ _nativeFatReportObservedRows.Remove(row);
+ }
+
+ foreach (var row in current)
+ {
+ if (!_nativeFatReportObservedRows.Add(row))
+ continue;
+ row.PropertyChanged += NativeFatReportRow_PropertyChanged;
+ }
+ }
+
+ private void NativeFatReportRow_PropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName is nameof(NativeFatSignalRow.Value1Text) or
+ nameof(NativeFatSignalRow.Value2Text) or
+ nameof(NativeFatSignalRow.Result) or
+ nameof(NativeFatSignalRow.HistoryText) or
+ nameof(NativeFatSignalRow.IsHistorical))
+ {
+ QueueNativeFatReportPreviewRefresh();
+ }
+ }
+
+ private void NativeFatReportScope_Changed(object sender, RoutedEventArgs e)
+ => QueueNativeFatReportPreviewRefresh();
+
+ private void QueueNativeFatReportPreviewRefresh()
+ {
+ if (!_nativeFatReportPreviewEnhanced || _nativeFatPreviewPane?.IsVisible != true || _nativeFatReportRefreshQueued)
+ return;
+
+ _nativeFatReportRefreshQueued = true;
+ Dispatcher.BeginInvoke(
+ DispatcherPriority.ContextIdle,
+ new Action(() =>
+ {
+ _nativeFatReportRefreshQueued = false;
+ RefreshNativeFatReportPreviewSnapshot();
+ }));
+ }
+
+ private void RefreshNativeFatReportPreviewSnapshot()
+ {
+ if (_nativeFatPreviewPane?.IsVisible != true || _nativeFatPreviewGrid == null)
+ return;
+
+ var device = SelectedDevice;
+ var state = _nativeFatCurrentState;
+ if (device == null || state == null ||
+ !state.DeviceId.Equals(device.DeviceId, StringComparison.OrdinalIgnoreCase))
+ {
+ _nativeFatReportSnapshot = null;
+ _nativeFatPreviewGrid.ItemsSource = null;
+ if (_nativeFatPreviewSummaryText != null)
+ _nativeFatPreviewSummaryText.Text = "Select an IED with a loaded FAT state.";
+ if (_nativeFatPreviewEvidenceText != null)
+ _nativeFatPreviewEvidenceText.Text = "No report evidence is loaded.";
+ return;
+ }
+
+ var previousReference = (_nativeFatPreviewGrid.SelectedItem as NativeFatReportRow)?.IecReference;
+ _nativeFatReportSnapshot = NativeFatReportSnapshotBuilder.Build(device, state);
+ var showHistorical = _nativeFatShowHistoricalCheck?.IsChecked == true;
+ var visibleRows = _nativeFatReportSnapshot.Rows
+ .Where(row => showHistorical || !row.IsHistorical)
+ .ToArray();
+ _nativeFatPreviewGrid.ItemsSource = visibleRows;
+
+ if (_nativeFatPreviewDeviceText != null)
+ _nativeFatPreviewDeviceText.Text = $"IED · {_nativeFatReportSnapshot.IedName} · {_nativeFatReportSnapshot.IpAddress}:{_nativeFatReportSnapshot.Port}";
+ if (_nativeFatPreviewSummaryText != null)
+ {
+ var scopeSuffix = !showHistorical && _nativeFatReportSnapshot.HistoricalCount > 0
+ ? " · historical hidden"
+ : string.Empty;
+ _nativeFatPreviewSummaryText.Text = _nativeFatReportSnapshot.SummaryText + scopeSuffix;
+ }
+
+ NativeFatReportRow? selection = null;
+ if (!string.IsNullOrWhiteSpace(previousReference))
+ {
+ selection = visibleRows.FirstOrDefault(row =>
+ row.IecReference.Equals(previousReference, StringComparison.OrdinalIgnoreCase));
+ }
+ selection ??= visibleRows.FirstOrDefault();
+ if (selection != null)
+ {
+ _nativeFatPreviewGrid.SelectedItem = selection;
+ _nativeFatPreviewGrid.ScrollIntoView(selection);
+ }
+ else
+ {
+ UpdateNativeFatPreviewEvidence(null);
+ }
+ }
+
+ private void NativeFatPreviewGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ => UpdateNativeFatPreviewEvidence(_nativeFatPreviewGrid?.SelectedItem as NativeFatReportRow);
+
+ private void UpdateNativeFatPreviewEvidence(NativeFatReportRow? row)
+ {
+ if (_nativeFatPreviewEvidenceText == null)
+ return;
+
+ _nativeFatPreviewEvidenceText.Text = row?.EvidenceSummaryText ??
+ "Select a report row to inspect captured evidence.";
+ }
+
+ private void NativeFatReportPreview_MainWindowClosed(object? sender, EventArgs e)
+ {
+ _nativeFatReportPreviewInstallRetry?.Stop();
+ if (_nativeFatPreviewGrid != null)
+ _nativeFatPreviewGrid.SelectionChanged -= NativeFatPreviewGrid_SelectionChanged;
+ if (_nativeFatPreviewPane != null)
+ _nativeFatPreviewPane.IsVisibleChanged -= NativeFatPreviewPane_IsVisibleChanged;
+ _nativeFatRows.CollectionChanged -= NativeFatReportRows_CollectionChanged;
+ if (_nativeFatShowHistoricalCheck != null)
+ {
+ _nativeFatShowHistoricalCheck.Checked -= NativeFatReportScope_Changed;
+ _nativeFatShowHistoricalCheck.Unchecked -= NativeFatReportScope_Changed;
+ }
+
+ foreach (var row in _nativeFatReportObservedRows)
+ row.PropertyChanged -= NativeFatReportRow_PropertyChanged;
+ _nativeFatReportObservedRows.Clear();
+ Closed -= NativeFatReportPreview_MainWindowClosed;
+ }
+}
diff --git a/MainWindow.NativeFatWorkspace.cs b/MainWindow.NativeFatWorkspace.cs
new file mode 100644
index 000000000..3c28770ea
--- /dev/null
+++ b/MainWindow.NativeFatWorkspace.cs
@@ -0,0 +1,980 @@
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Data;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+using System.Windows.Threading;
+using ArIED61850Tester.Models;
+using ArIED61850Tester.Services;
+
+namespace ArIED61850Tester;
+
+///
+/// Native continuous FAT workspace layered on the persistent P0/P1 Engineering shell.
+///
+/// Authority boundary:
+/// - IED Explorer owns SelectedDevice, signal engineering, live value and acquisition.
+/// - the shared Command Dock remains the only command UI/runtime owner.
+/// - this workspace owns only FAT captures, result/history and per-IED persistence.
+///
+/// The legacy IoListTestingWindow remains untouched as a compatibility fallback while
+/// report/evidence parity is migrated incrementally.
+///
+public partial class MainWindow
+{
+ private const int NativeFatWorkspaceIndex = 6;
+
+ private readonly NativeFatStateStore _nativeFatStore = new();
+ private readonly ObservableCollection _nativeFatRows = new();
+ private readonly Dictionary _nativeFatStateCache = new(StringComparer.OrdinalIgnoreCase);
+ private readonly SemaphoreSlim _nativeFatSaveGate = new(1, 1);
+
+ private DispatcherTimer? _nativeFatInstallRetry;
+ private DispatcherTimer? _nativeFatReconcileTimer;
+ private DispatcherTimer? _nativeFatSaveTimer;
+ private CancellationTokenSource? _nativeFatLoadCts;
+ private TabItem? _nativeFatTab;
+ private Button? _nativeFatNavButton;
+ private DataGrid? _nativeFatGrid;
+ private DataGrid? _nativeFatPreviewGrid;
+ private ICollectionView? _nativeFatView;
+ private TextBox? _nativeFatSearchBox;
+ private CheckBox? _nativeFatShowHistoricalCheck;
+ private Border? _nativeFatPreviewPane;
+ private ColumnDefinition? _nativeFatPreviewGapColumn;
+ private ColumnDefinition? _nativeFatPreviewColumn;
+ private TextBlock? _nativeFatContextText;
+ private TextBlock? _nativeFatStatusText;
+ private TextBlock? _nativeFatPreviewDeviceText;
+ private TextBlock? _nativeFatPreviewSummaryText;
+ private TextBlock? _nativeFatPersistenceText;
+ private Iec61850MonitorDevice? _nativeFatObservedDevice;
+ private NativeFatDeviceState? _nativeFatCurrentState;
+ private bool _nativeFatInstalled;
+ private bool _nativeFatLoading;
+ private bool _nativeFatPreviewVisible;
+
+ [ModuleInitializer]
+ internal static void RegisterNativeFatWorkspace()
+ {
+ EventManager.RegisterClassHandler(
+ typeof(MainWindow),
+ FrameworkElement.LoadedEvent,
+ new RoutedEventHandler(NativeFatWorkspace_MainWindowLoaded),
+ handledEventsToo: true);
+ }
+
+ private static void NativeFatWorkspace_MainWindowLoaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is not MainWindow window || window._nativeFatInstalled)
+ return;
+
+ // P0 moves MainTabs/Explorer/Command Dock at ContextIdle. Install FAT after that
+ // re-parenting has settled so this stacked feature never races the stable P0 shell.
+ window.Dispatcher.BeginInvoke(
+ DispatcherPriority.ApplicationIdle,
+ new Action(window.TryInstallNativeFatWorkspace));
+ }
+
+ private void TryInstallNativeFatWorkspace()
+ {
+ if (_nativeFatInstalled || !IsLoaded)
+ return;
+
+ if (_persistentWorkbench == null || MainTabs.Items.Count < 6)
+ {
+ _nativeFatInstallRetry ??= new DispatcherTimer(DispatcherPriority.ApplicationIdle)
+ {
+ Interval = TimeSpan.FromMilliseconds(140)
+ };
+ _nativeFatInstallRetry.Tick -= NativeFatInstallRetry_Tick;
+ _nativeFatInstallRetry.Tick += NativeFatInstallRetry_Tick;
+ _nativeFatInstallRetry.Start();
+ return;
+ }
+
+ _nativeFatInstallRetry?.Stop();
+ _nativeFatInstalled = true;
+
+ _nativeFatTab = new TabItem
+ {
+ Header = "FAT",
+ Content = BuildNativeFatWorkspaceContent()
+ };
+ MainTabs.Items.Add(_nativeFatTab);
+
+ // The command dock is deliberately shared, not cloned. FAT is command-centric,
+ // therefore start with the same expanded behavior as Explorer/Event Log.
+ _persistentWorkbench.DockExpandedByWorkspace[NativeFatWorkspaceIndex] = true;
+
+ InstallNativeFatNavigationButton();
+ InstallNativeFatTimers();
+ AttachNativeFatObservedDevice(SelectedDevice);
+
+ PropertyChanged += NativeFat_MainWindowPropertyChanged;
+ MainTabs.SelectionChanged += NativeFat_MainTabsSelectionChanged;
+ SizeChanged += NativeFat_MainWindowSizeChanged;
+ Closed += NativeFat_MainWindowClosed;
+
+ QueueNativeFatNavigationGeometry();
+ UpdateNativeFatSummary();
+ }
+
+ private void NativeFatInstallRetry_Tick(object? sender, EventArgs e)
+ {
+ _nativeFatInstallRetry?.Stop();
+ TryInstallNativeFatWorkspace();
+ }
+
+ private UIElement BuildNativeFatWorkspaceContent()
+ {
+ var root = new Grid { Margin = new Thickness(0) };
+ root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(8) });
+ root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(8) });
+ root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
+
+ var header = BuildNativeFatHeader();
+ Grid.SetRow(header, 0);
+ root.Children.Add(header);
+
+ var toolbar = BuildNativeFatToolbar();
+ Grid.SetRow(toolbar, 2);
+ root.Children.Add(toolbar);
+
+ var body = BuildNativeFatBody();
+ Grid.SetRow(body, 4);
+ root.Children.Add(body);
+ return root;
+ }
+
+ private Border BuildNativeFatHeader()
+ {
+ var border = new Border
+ {
+ Padding = new Thickness(14, 10, 14, 10),
+ CornerRadius = new CornerRadius(12),
+ Background = ResourceBrush("SurfaceElevated", Colors.White),
+ BorderBrush = ResourceBrush("Line", Color.FromRgb(0xDD, 0xE5, 0xF0)),
+ BorderThickness = new Thickness(1)
+ };
+
+ var grid = new Grid();
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+
+ var titleStack = new StackPanel();
+ titleStack.Children.Add(new TextBlock
+ {
+ Text = "FAT · Continuous Testing",
+ FontSize = 14.5,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = ResourceBrush("Ink", Color.FromRgb(0x20, 0x30, 0x4A))
+ });
+ titleStack.Children.Add(new TextBlock
+ {
+ Text = "Explorer signal authority · live values stay shared · captures autosave per IED · removed signals keep their history",
+ Margin = new Thickness(0, 3, 0, 0),
+ FontSize = 10.8,
+ Foreground = ResourceBrush("Muted", Color.FromRgb(0x66, 0x75, 0x8B)),
+ TextWrapping = TextWrapping.Wrap
+ });
+ grid.Children.Add(titleStack);
+
+ _nativeFatContextText = new TextBlock
+ {
+ Text = "IED · NONE",
+ FontSize = 10.5,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = ResourceBrush("Accent", Color.FromRgb(0x25, 0x63, 0xEB)),
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(14, 0, 0, 0)
+ };
+ Grid.SetColumn(_nativeFatContextText, 1);
+ grid.Children.Add(_nativeFatContextText);
+
+ border.Child = grid;
+ return border;
+ }
+
+ private Border BuildNativeFatToolbar()
+ {
+ var border = new Border
+ {
+ Padding = new Thickness(9, 7, 9, 7),
+ CornerRadius = new CornerRadius(11),
+ Background = ResourceBrush("Surface", Colors.White),
+ BorderBrush = ResourceBrush("Line", Color.FromRgb(0xDD, 0xE5, 0xF0)),
+ BorderThickness = new Thickness(1)
+ };
+
+ var wrap = new WrapPanel
+ {
+ Orientation = Orientation.Horizontal,
+ VerticalAlignment = VerticalAlignment.Center
+ };
+
+ _nativeFatSearchBox = new TextBox
+ {
+ Width = 210,
+ Height = 30,
+ Margin = new Thickness(0, 0, 8, 0),
+ Padding = new Thickness(8, 4, 8, 4),
+ VerticalContentAlignment = VerticalAlignment.Center,
+ ToolTip = "Search Signal, IEC reference, type, value, result or history state"
+ };
+ _nativeFatSearchBox.TextChanged += NativeFatSearchBox_TextChanged;
+ wrap.Children.Add(_nativeFatSearchBox);
+
+ wrap.Children.Add(CreateNativeFatButton("Refresh", NativeFatRefresh_Click, "Reconcile the saved FAT state with the current Explorer signal scope."));
+ wrap.Children.Add(CreateNativeFatButton("Capture V1", NativeFatCapture1_Click, "Capture the current Explorer live value into Value 1 for selected FAT rows."));
+ wrap.Children.Add(CreateNativeFatButton("Capture V2", NativeFatCapture2_Click, "Capture the current Explorer live value into Value 2 for selected FAT rows."));
+ wrap.Children.Add(CreateNativeFatButton("PASS", NativeFatPass_Click, "Mark selected FAT rows PASS; previous results remain in history."));
+ wrap.Children.Add(CreateNativeFatButton("REVIEW", NativeFatReview_Click, "Mark selected FAT rows REVIEW; previous results remain in history."));
+ wrap.Children.Add(CreateNativeFatButton("FAIL", NativeFatFail_Click, "Mark selected FAT rows FAIL; previous results remain in history."));
+ wrap.Children.Add(CreateNativeFatButton("Reset current", NativeFatReset_Click, "Clear current captures/result while retaining the previous state in history."));
+ wrap.Children.Add(CreateNativeFatButton("Report Preview", NativeFatReportPreview_Click, "Show or hide the native FAT report preview without opening another FAT runtime."));
+
+ _nativeFatShowHistoricalCheck = new CheckBox
+ {
+ Content = "Show historical",
+ IsChecked = false,
+ Margin = new Thickness(7, 6, 7, 0),
+ VerticalAlignment = VerticalAlignment.Center,
+ FontSize = 10.7,
+ Foreground = ResourceBrush("Muted", Color.FromRgb(0x66, 0x75, 0x8B)),
+ ToolTip = "Show signals removed from the current Explorer/SCL scope. Their previous FAT evidence is never deleted."
+ };
+ _nativeFatShowHistoricalCheck.Checked += NativeFatShowHistorical_Changed;
+ _nativeFatShowHistoricalCheck.Unchecked += NativeFatShowHistorical_Changed;
+ wrap.Children.Add(_nativeFatShowHistoricalCheck);
+
+ _nativeFatStatusText = new TextBlock
+ {
+ Text = "Select an IED to begin.",
+ Margin = new Thickness(9, 7, 0, 0),
+ VerticalAlignment = VerticalAlignment.Center,
+ FontSize = 10.4,
+ Foreground = ResourceBrush("Muted", Color.FromRgb(0x66, 0x75, 0x8B))
+ };
+ wrap.Children.Add(_nativeFatStatusText);
+
+ border.Child = wrap;
+ return border;
+ }
+
+ private Grid BuildNativeFatBody()
+ {
+ var body = new Grid();
+ body.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star), MinWidth = 0 });
+ _nativeFatPreviewGapColumn = new ColumnDefinition { Width = new GridLength(0) };
+ _nativeFatPreviewColumn = new ColumnDefinition { Width = new GridLength(0), MinWidth = 0 };
+ body.ColumnDefinitions.Add(_nativeFatPreviewGapColumn);
+ body.ColumnDefinitions.Add(_nativeFatPreviewColumn);
+
+ _nativeFatGrid = BuildNativeFatGrid(preview: false);
+ Grid.SetColumn(_nativeFatGrid, 0);
+ body.Children.Add(_nativeFatGrid);
+
+ _nativeFatPreviewPane = BuildNativeFatPreviewPane();
+ _nativeFatPreviewPane.Visibility = Visibility.Collapsed;
+ Grid.SetColumn(_nativeFatPreviewPane, 2);
+ body.Children.Add(_nativeFatPreviewPane);
+ return body;
+ }
+
+ private DataGrid BuildNativeFatGrid(bool preview)
+ {
+ var grid = new DataGrid
+ {
+ AutoGenerateColumns = false,
+ CanUserAddRows = false,
+ CanUserDeleteRows = false,
+ CanUserReorderColumns = true,
+ CanUserResizeColumns = true,
+ HeadersVisibility = DataGridHeadersVisibility.Column,
+ IsReadOnly = true,
+ SelectionMode = preview ? DataGridSelectionMode.Single : DataGridSelectionMode.Extended,
+ SelectionUnit = DataGridSelectionUnit.FullRow,
+ GridLinesVisibility = DataGridGridLinesVisibility.Horizontal,
+ HorizontalGridLinesBrush = ResourceBrush("Line", Color.FromRgb(0xE2, 0xE8, 0xF0)),
+ BorderBrush = ResourceBrush("Line", Color.FromRgb(0xDD, 0xE5, 0xF0)),
+ BorderThickness = new Thickness(1),
+ RowHeight = preview ? 29 : 32,
+ ColumnHeaderHeight = 31,
+ Background = ResourceBrush("Surface", Colors.White),
+ AlternatingRowBackground = new SolidColorBrush(Color.FromRgb(0xFA, 0xFB, 0xFD)),
+ EnableRowVirtualization = true,
+ EnableColumnVirtualization = true
+ };
+
+ if (TryFindResource("DataGridHeaderCompact") is Style headerStyle)
+ grid.ColumnHeaderStyle = headerStyle;
+ if (TryFindResource("DataGridCellCompact") is Style cellStyle)
+ grid.CellStyle = cellStyle;
+
+ var rowStyle = new Style(typeof(DataGridRow));
+ rowStyle.Setters.Add(new Setter(Control.FontSizeProperty, preview ? 10.0 : 10.4));
+ var historicalTrigger = new DataTrigger
+ {
+ Binding = new Binding(nameof(NativeFatSignalRow.IsHistorical)),
+ Value = true
+ };
+ historicalTrigger.Setters.Add(new Setter(UIElement.OpacityProperty, 0.56));
+ rowStyle.Triggers.Add(historicalTrigger);
+ grid.RowStyle = rowStyle;
+
+ if (!preview)
+ {
+ grid.Columns.Add(TextColumn("SIGNAL", nameof(NativeFatSignalRow.SignalName), 1.25, minWidth: 130));
+ grid.Columns.Add(TextColumn("IEC REFERENCE", nameof(NativeFatSignalRow.IecReference), 1.85, minWidth: 190));
+ grid.Columns.Add(TextColumn("TYPE", nameof(NativeFatSignalRow.DataType), 0.72, minWidth: 72));
+ grid.Columns.Add(TextColumn("LIVE VALUE", nameof(NativeFatSignalRow.LiveValue), 0.9, minWidth: 90));
+ grid.Columns.Add(TextColumn("QUALITY", nameof(NativeFatSignalRow.Quality), 0.82, minWidth: 82));
+ grid.Columns.Add(TextColumn("VALUE 1", nameof(NativeFatSignalRow.Value1Text), 0.78, minWidth: 78));
+ grid.Columns.Add(TextColumn("VALUE 2", nameof(NativeFatSignalRow.Value2Text), 0.78, minWidth: 78));
+ grid.Columns.Add(TextColumn("STATUS", nameof(NativeFatSignalRow.StatusText), 0.72, minWidth: 76));
+ grid.Columns.Add(TextColumn("RESULT", nameof(NativeFatSignalRow.Result), 0.72, minWidth: 72));
+ grid.Columns.Add(TextColumn("HISTORY", nameof(NativeFatSignalRow.HistoryText), 0.78, minWidth: 78));
+ }
+ else
+ {
+ grid.Columns.Add(TextColumn("SIGNAL", nameof(NativeFatSignalRow.SignalName), 1.35, minWidth: 115));
+ grid.Columns.Add(TextColumn("V1", nameof(NativeFatSignalRow.Value1Text), 0.65, minWidth: 62));
+ grid.Columns.Add(TextColumn("V2", nameof(NativeFatSignalRow.Value2Text), 0.65, minWidth: 62));
+ grid.Columns.Add(TextColumn("RESULT", nameof(NativeFatSignalRow.Result), 0.72, minWidth: 68));
+ }
+
+ return grid;
+ }
+
+ private Border BuildNativeFatPreviewPane()
+ {
+ var border = new Border
+ {
+ Padding = new Thickness(11),
+ CornerRadius = new CornerRadius(12),
+ Background = ResourceBrush("SurfaceElevated", Colors.White),
+ BorderBrush = ResourceBrush("Line", Color.FromRgb(0xDD, 0xE5, 0xF0)),
+ BorderThickness = new Thickness(1)
+ };
+
+ var grid = new Grid();
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+ grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(8) });
+ grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
+ grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
+
+ var title = new TextBlock
+ {
+ Text = "FAT Report Preview",
+ FontSize = 13,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = ResourceBrush("Ink", Color.FromRgb(0x20, 0x30, 0x4A))
+ };
+ grid.Children.Add(title);
+
+ _nativeFatPreviewDeviceText = new TextBlock
+ {
+ Text = "IED · NONE",
+ Margin = new Thickness(0, 4, 0, 0),
+ FontSize = 10.6,
+ FontWeight = FontWeights.SemiBold,
+ Foreground = ResourceBrush("Accent", Color.FromRgb(0x25, 0x63, 0xEB))
+ };
+ Grid.SetRow(_nativeFatPreviewDeviceText, 1);
+ grid.Children.Add(_nativeFatPreviewDeviceText);
+
+ _nativeFatPreviewSummaryText = new TextBlock
+ {
+ Text = "No FAT state loaded.",
+ Margin = new Thickness(0, 4, 0, 0),
+ FontSize = 10.2,
+ Foreground = ResourceBrush("Muted", Color.FromRgb(0x66, 0x75, 0x8B)),
+ TextWrapping = TextWrapping.Wrap
+ };
+ Grid.SetRow(_nativeFatPreviewSummaryText, 2);
+ grid.Children.Add(_nativeFatPreviewSummaryText);
+
+ _nativeFatPreviewGrid = BuildNativeFatGrid(preview: true);
+ Grid.SetRow(_nativeFatPreviewGrid, 4);
+ grid.Children.Add(_nativeFatPreviewGrid);
+
+ _nativeFatPersistenceText = new TextBlock
+ {
+ Text = "Per-IED JSON · non-destructive reconciliation",
+ Margin = new Thickness(0, 7, 0, 0),
+ FontSize = 9.5,
+ Foreground = ResourceBrush("Muted", Color.FromRgb(0x66, 0x75, 0x8B)),
+ TextWrapping = TextWrapping.Wrap
+ };
+ Grid.SetRow(_nativeFatPersistenceText, 5);
+ grid.Children.Add(_nativeFatPersistenceText);
+
+ border.Child = grid;
+ return border;
+ }
+
+ private DataGridTextColumn TextColumn(string header, string path, double star, double minWidth)
+ => new()
+ {
+ Header = header,
+ Binding = new Binding(path) { Mode = BindingMode.OneWay },
+ Width = new DataGridLength(star, DataGridLengthUnitType.Star),
+ MinWidth = minWidth
+ };
+
+ private Button CreateNativeFatButton(string text, RoutedEventHandler click, string toolTip)
+ {
+ var button = new Button
+ {
+ Content = text,
+ Margin = new Thickness(0, 0, 6, 0),
+ Padding = new Thickness(9, 5, 9, 5),
+ MinHeight = 29,
+ FontSize = 10.5,
+ ToolTip = toolTip
+ };
+ if (TryFindResource("SoftButton") is Style style)
+ button.Style = style;
+ button.Click += click;
+ return button;
+ }
+
+ private void InstallNativeFatNavigationButton()
+ {
+ if (_nativeFatNavButton != null)
+ return;
+
+ while (WorkflowNavGrid.ColumnDefinitions.Count < 7)
+ WorkflowNavGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+
+ Grid.SetColumnSpan(WorkflowPill, 7);
+ _nativeFatNavButton = new Button
+ {
+ Name = "NavNativeFatButton",
+ Content = "FAT",
+ Tag = NativeFatWorkspaceIndex,
+ MinWidth = 0,
+ ToolTip = "Continuous FAT testing using the selected IED and live Explorer signal authority"
+ };
+ if (TryFindResource("SegmentedNavButton") is Style navStyle)
+ _nativeFatNavButton.Style = navStyle;
+ _nativeFatNavButton.Click += NativeFatNavButton_Click;
+ Grid.SetColumn(_nativeFatNavButton, 6);
+ WorkflowNavGrid.Children.Add(_nativeFatNavButton);
+ }
+
+ private void InstallNativeFatTimers()
+ {
+ _nativeFatReconcileTimer = new DispatcherTimer(DispatcherPriority.Background)
+ {
+ Interval = TimeSpan.FromMilliseconds(320)
+ };
+ _nativeFatReconcileTimer.Tick += NativeFatReconcileTimer_Tick;
+
+ _nativeFatSaveTimer = new DispatcherTimer(DispatcherPriority.Background)
+ {
+ Interval = TimeSpan.FromMilliseconds(350)
+ };
+ _nativeFatSaveTimer.Tick += NativeFatSaveTimer_Tick;
+ }
+
+ private async void NativeFatNavButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (MainTabs.Items.Count <= NativeFatWorkspaceIndex)
+ return;
+
+ MainTabs.SelectedIndex = NativeFatWorkspaceIndex;
+ QueueNativeFatNavigationGeometry();
+ await EnsureNativeFatLoadedAsync(forceReconcile: false);
+ }
+
+ private void NativeFat_MainTabsSelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (!ReferenceEquals(e.Source, MainTabs))
+ return;
+
+ QueueNativeFatNavigationGeometry();
+ if (MainTabs.SelectedIndex == NativeFatWorkspaceIndex)
+ _ = EnsureNativeFatLoadedAsync(forceReconcile: false);
+ else
+ QueueNativeFatSave();
+ }
+
+ private void NativeFat_MainWindowSizeChanged(object sender, SizeChangedEventArgs e)
+ => QueueNativeFatNavigationGeometry();
+
+ private void QueueNativeFatNavigationGeometry()
+ {
+ if (!_nativeFatInstalled)
+ return;
+ Dispatcher.BeginInvoke(
+ DispatcherPriority.ApplicationIdle,
+ new Action(ApplyNativeFatNavigationGeometry));
+ }
+
+ private void ApplyNativeFatNavigationGeometry()
+ {
+ if (!_nativeFatInstalled || _nativeFatNavButton == null)
+ return;
+
+ var availableWidth = ActualWidth > 0d ? ActualWidth : 1480d;
+ var wide = availableWidth >= 1700d;
+ var medium = availableWidth >= 1380d;
+ var shellWidth = wide ? 1085d : medium ? 995d : 805d;
+
+ WorkflowNavShell.Width = shellWidth;
+ WorkflowNavShell.MinWidth = shellWidth;
+ WorkflowNavShell.Height = 60;
+ WorkflowNavShell.Padding = new Thickness(5, 6, 5, 6);
+ WorkflowNavGrid.ClipToBounds = false;
+
+ while (WorkflowNavGrid.ColumnDefinitions.Count < 7)
+ WorkflowNavGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ foreach (var column in WorkflowNavGrid.ColumnDefinitions)
+ column.Width = new GridLength(1, GridUnitType.Star);
+
+ var buttons = new[]
+ {
+ NavExplorerButton,
+ NavLiveButton,
+ NavEventsButton,
+ NavAlarmButton,
+ NavGooseButton,
+ NavDiagnosticsButton,
+ _nativeFatNavButton
+ };
+ for (var index = 0; index < buttons.Length; index++)
+ {
+ var button = buttons[index];
+ button.MinHeight = 40;
+ button.MinWidth = 0;
+ button.Margin = new Thickness(1);
+ button.Padding = wide ? new Thickness(9, 7, 9, 7) : new Thickness(5, 7, 5, 7);
+ button.HorizontalContentAlignment = HorizontalAlignment.Center;
+ button.VerticalContentAlignment = VerticalAlignment.Center;
+ button.Foreground = index == MainTabs.SelectedIndex
+ ? Brushes.White
+ : ResourceBrush("Muted", Color.FromRgb(0x5F, 0x6B, 0x7A));
+ }
+
+ Grid.SetColumnSpan(WorkflowPill, 7);
+ var contentWidth = Math.Max(0d, shellWidth - WorkflowNavShell.Padding.Left - WorkflowNavShell.Padding.Right);
+ var cellWidth = contentWidth / 7d;
+ WorkflowPill.Width = Math.Max(1d, cellWidth - 2d);
+ WorkflowPill.Height = 36;
+
+ WorkflowPillTranslate.BeginAnimation(TranslateTransform.XProperty, null);
+ WorkflowPillTranslate.X = Math.Clamp(MainTabs.SelectedIndex, 0, NativeFatWorkspaceIndex) * cellWidth;
+ }
+
+ private void NativeFat_MainWindowPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName != nameof(SelectedDevice))
+ return;
+
+ QueueNativeFatSave();
+ AttachNativeFatObservedDevice(SelectedDevice);
+ if (MainTabs.SelectedIndex == NativeFatWorkspaceIndex)
+ _ = EnsureNativeFatLoadedAsync(forceReconcile: true);
+ else
+ UpdateNativeFatSummary();
+ }
+
+ private void AttachNativeFatObservedDevice(Iec61850MonitorDevice? device)
+ {
+ if (ReferenceEquals(_nativeFatObservedDevice, device))
+ return;
+
+ if (_nativeFatObservedDevice != null)
+ {
+ _nativeFatObservedDevice.Signals.CollectionChanged -= NativeFatSignalCollectionChanged;
+ _nativeFatObservedDevice.Points.CollectionChanged -= NativeFatSignalCollectionChanged;
+ }
+
+ _nativeFatObservedDevice = device;
+ if (_nativeFatObservedDevice != null)
+ {
+ _nativeFatObservedDevice.Signals.CollectionChanged += NativeFatSignalCollectionChanged;
+ _nativeFatObservedDevice.Points.CollectionChanged += NativeFatSignalCollectionChanged;
+ }
+ }
+
+ private void NativeFatSignalCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (MainTabs.SelectedIndex != NativeFatWorkspaceIndex)
+ return;
+ _nativeFatReconcileTimer?.Stop();
+ _nativeFatReconcileTimer?.Start();
+ }
+
+ private async void NativeFatReconcileTimer_Tick(object? sender, EventArgs e)
+ {
+ _nativeFatReconcileTimer?.Stop();
+ await EnsureNativeFatLoadedAsync(forceReconcile: true);
+ }
+
+ private async Task EnsureNativeFatLoadedAsync(bool forceReconcile)
+ {
+ if (!_nativeFatInstalled || MainTabs.SelectedIndex != NativeFatWorkspaceIndex)
+ return;
+
+ var device = SelectedDevice;
+ if (device == null)
+ {
+ ClearNativeFatRows();
+ _nativeFatCurrentState = null;
+ UpdateNativeFatSummary();
+ return;
+ }
+
+ var cacheKey = StableNativeFatDeviceKey(device);
+ if (!forceReconcile && _nativeFatCurrentState != null &&
+ _nativeFatCurrentState.DeviceId.Equals(device.DeviceId, StringComparison.OrdinalIgnoreCase))
+ {
+ UpdateNativeFatSummary();
+ return;
+ }
+
+ _nativeFatLoadCts?.Cancel();
+ _nativeFatLoadCts?.Dispose();
+ _nativeFatLoadCts = CancellationTokenSource.CreateLinkedTokenSource(_applicationCancellation.Token);
+ var token = _nativeFatLoadCts.Token;
+ _nativeFatLoading = true;
+ SetNativeFatStatus($"Loading FAT state for {device.Name}…");
+
+ try
+ {
+ NativeFatDeviceState state;
+ if (_nativeFatStateCache.TryGetValue(cacheKey, out var cached))
+ {
+ state = cached;
+ }
+ else
+ {
+ state = await _nativeFatStore.LoadAsync(device, token);
+ token.ThrowIfCancellationRequested();
+ _nativeFatStateCache[cacheKey] = state;
+ }
+
+ // We are back on the WPF dispatcher here. Reconciliation enumerates Explorer
+ // ObservableCollections only on their owning UI thread.
+ token.ThrowIfCancellationRequested();
+ if (!ReferenceEquals(device, SelectedDevice))
+ return;
+
+ NativeFatStateStore.Reconcile(state, device);
+ _nativeFatCurrentState = state;
+ RebuildNativeFatRows(state, device);
+ QueueNativeFatSave();
+ SetNativeFatStatus($"FAT ready · {device.Name}");
+ }
+ catch (OperationCanceledException)
+ {
+ // Fast IED switches are normal; stale loads are intentionally discarded.
+ }
+ catch (Exception ex)
+ {
+ AddLog("WARN", "Native FAT", $"FAT state load/reconcile failed: {ex.Message}");
+ SetNativeFatStatus("FAT state could not be loaded. Explorer monitoring remains unaffected.");
+ }
+ finally
+ {
+ _nativeFatLoading = false;
+ UpdateNativeFatSummary();
+ }
+ }
+
+ private void RebuildNativeFatRows(NativeFatDeviceState state, Iec61850MonitorDevice device)
+ {
+ ClearNativeFatRows();
+ foreach (var row in NativeFatStateStore.BuildRows(state, device))
+ {
+ row.StateChanged += NativeFatRow_StateChanged;
+ _nativeFatRows.Add(row);
+ }
+
+ _nativeFatView = CollectionViewSource.GetDefaultView(_nativeFatRows);
+ _nativeFatView.Filter = NativeFatViewFilter;
+ if (_nativeFatGrid != null)
+ _nativeFatGrid.ItemsSource = _nativeFatView;
+ if (_nativeFatPreviewGrid != null)
+ _nativeFatPreviewGrid.ItemsSource = _nativeFatRows;
+ UpdateNativeFatSummary();
+ }
+
+ private void ClearNativeFatRows()
+ {
+ foreach (var row in _nativeFatRows)
+ {
+ row.StateChanged -= NativeFatRow_StateChanged;
+ row.Dispose();
+ }
+ _nativeFatRows.Clear();
+ _nativeFatView = null;
+ if (_nativeFatGrid != null)
+ _nativeFatGrid.ItemsSource = null;
+ if (_nativeFatPreviewGrid != null)
+ _nativeFatPreviewGrid.ItemsSource = null;
+ }
+
+ private bool NativeFatViewFilter(object item)
+ {
+ if (item is not NativeFatSignalRow row)
+ return false;
+
+ if (row.IsHistorical && _nativeFatShowHistoricalCheck?.IsChecked != true)
+ return false;
+
+ var query = _nativeFatSearchBox?.Text?.Trim();
+ if (string.IsNullOrWhiteSpace(query))
+ return true;
+
+ return row.SignalName.Contains(query, StringComparison.OrdinalIgnoreCase) ||
+ row.IecReference.Contains(query, StringComparison.OrdinalIgnoreCase) ||
+ row.IecTelegram.Contains(query, StringComparison.OrdinalIgnoreCase) ||
+ row.DataType.Contains(query, StringComparison.OrdinalIgnoreCase) ||
+ row.LiveValue.Contains(query, StringComparison.OrdinalIgnoreCase) ||
+ row.Value1Text.Contains(query, StringComparison.OrdinalIgnoreCase) ||
+ row.Value2Text.Contains(query, StringComparison.OrdinalIgnoreCase) ||
+ row.Result.Contains(query, StringComparison.OrdinalIgnoreCase) ||
+ row.StatusText.Contains(query, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private void NativeFatSearchBox_TextChanged(object sender, TextChangedEventArgs e)
+ => _nativeFatView?.Refresh();
+
+ private void NativeFatShowHistorical_Changed(object sender, RoutedEventArgs e)
+ {
+ _nativeFatView?.Refresh();
+ UpdateNativeFatSummary();
+ }
+
+ private void NativeFatRow_StateChanged(object? sender, EventArgs e)
+ {
+ QueueNativeFatSave();
+ UpdateNativeFatSummary();
+ _nativeFatPreviewGrid?.Items.Refresh();
+ }
+
+ private IReadOnlyList SelectedNativeFatRows()
+ {
+ if (_nativeFatGrid == null)
+ return Array.Empty();
+
+ var selected = _nativeFatGrid.SelectedItems.OfType().ToArray();
+ if (selected.Length > 0)
+ return selected;
+ return _nativeFatGrid.CurrentItem is NativeFatSignalRow current
+ ? new[] { current }
+ : Array.Empty();
+ }
+
+ private void NativeFatRefresh_Click(object sender, RoutedEventArgs e)
+ => _ = EnsureNativeFatLoadedAsync(forceReconcile: true);
+
+ private void NativeFatCapture1_Click(object sender, RoutedEventArgs e)
+ => CaptureNativeFatSelection(1);
+
+ private void NativeFatCapture2_Click(object sender, RoutedEventArgs e)
+ => CaptureNativeFatSelection(2);
+
+ private void CaptureNativeFatSelection(int slot)
+ {
+ var rows = SelectedNativeFatRows();
+ if (rows.Count == 0)
+ {
+ SetNativeFatStatus("Select one or more FAT signals first.");
+ return;
+ }
+
+ var captured = 0;
+ foreach (var row in rows)
+ {
+ if (row.CaptureValue(slot))
+ captured++;
+ }
+ SetNativeFatStatus(captured == 0
+ ? "No selected row has a current Explorer signal source. Historical rows cannot be captured."
+ : $"Captured Value {slot} for {captured} signal(s). Autosave queued.");
+ }
+
+ private void NativeFatPass_Click(object sender, RoutedEventArgs e)
+ => SetNativeFatSelectionResult(NativeFatResult.Pass);
+
+ private void NativeFatReview_Click(object sender, RoutedEventArgs e)
+ => SetNativeFatSelectionResult(NativeFatResult.Review);
+
+ private void NativeFatFail_Click(object sender, RoutedEventArgs e)
+ => SetNativeFatSelectionResult(NativeFatResult.Fail);
+
+ private void SetNativeFatSelectionResult(string result)
+ {
+ var rows = SelectedNativeFatRows();
+ if (rows.Count == 0)
+ {
+ SetNativeFatStatus("Select one or more FAT signals first.");
+ return;
+ }
+
+ foreach (var row in rows)
+ row.SetResult(result);
+ SetNativeFatStatus($"{rows.Count} signal(s) marked {result}. Previous state retained in history.");
+ }
+
+ private void NativeFatReset_Click(object sender, RoutedEventArgs e)
+ {
+ var rows = SelectedNativeFatRows();
+ if (rows.Count == 0)
+ {
+ SetNativeFatStatus("Select one or more FAT signals first.");
+ return;
+ }
+
+ foreach (var row in rows)
+ row.ResetCurrentResult();
+ SetNativeFatStatus($"Reset current FAT state for {rows.Count} signal(s); history was retained.");
+ }
+
+ private void NativeFatReportPreview_Click(object sender, RoutedEventArgs e)
+ {
+ _nativeFatPreviewVisible = !_nativeFatPreviewVisible;
+ if (_nativeFatPreviewPane == null || _nativeFatPreviewGapColumn == null || _nativeFatPreviewColumn == null)
+ return;
+
+ _nativeFatPreviewPane.Visibility = _nativeFatPreviewVisible ? Visibility.Visible : Visibility.Collapsed;
+ _nativeFatPreviewGapColumn.Width = new GridLength(_nativeFatPreviewVisible ? 10 : 0);
+ _nativeFatPreviewColumn.Width = _nativeFatPreviewVisible ? new GridLength(330) : new GridLength(0);
+ UpdateNativeFatSummary();
+ }
+
+ private void QueueNativeFatSave()
+ {
+ if (_nativeFatCurrentState == null || _nativeFatSaveTimer == null)
+ return;
+ _nativeFatSaveTimer.Stop();
+ _nativeFatSaveTimer.Start();
+ }
+
+ private async void NativeFatSaveTimer_Tick(object? sender, EventArgs e)
+ {
+ _nativeFatSaveTimer?.Stop();
+ if (_nativeFatCurrentState != null)
+ await SaveNativeFatStateAsync(_nativeFatCurrentState);
+ }
+
+ private async Task SaveNativeFatStateAsync(NativeFatDeviceState state)
+ {
+ try
+ {
+ await _nativeFatSaveGate.WaitAsync(_applicationCancellation.Token);
+ try
+ {
+ await _nativeFatStore.SaveAsync(state, _applicationCancellation.Token);
+ }
+ finally
+ {
+ _nativeFatSaveGate.Release();
+ }
+
+ if (ReferenceEquals(state, _nativeFatCurrentState))
+ {
+ SetNativeFatStatus($"Saved · {DateTime.Now:HH:mm:ss}");
+ UpdateNativeFatSummary();
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Application shutdown or superseded state.
+ }
+ catch (Exception ex)
+ {
+ AddLog("WARN", "Native FAT", $"Autosave failed: {ex.Message}");
+ if (ReferenceEquals(state, _nativeFatCurrentState))
+ SetNativeFatStatus("Autosave failed; current in-memory FAT state is still available.");
+ }
+ }
+
+ private void UpdateNativeFatSummary()
+ {
+ var device = SelectedDevice;
+ var deviceLabel = device == null ? "NONE" : device.Name;
+ if (_nativeFatContextText != null)
+ _nativeFatContextText.Text = $"IED · {deviceLabel}";
+ if (_nativeFatPreviewDeviceText != null)
+ _nativeFatPreviewDeviceText.Text = $"IED · {deviceLabel}";
+
+ var current = _nativeFatRows.Count(row => !row.IsHistorical);
+ var historical = _nativeFatRows.Count(row => row.IsHistorical);
+ var pass = _nativeFatRows.Count(row => !row.IsHistorical && row.Result == NativeFatResult.Pass);
+ var review = _nativeFatRows.Count(row => !row.IsHistorical && row.Result == NativeFatResult.Review);
+ var fail = _nativeFatRows.Count(row => !row.IsHistorical && row.Result == NativeFatResult.Fail);
+ var untested = Math.Max(0, current - pass - review - fail);
+
+ if (_nativeFatPreviewSummaryText != null)
+ {
+ _nativeFatPreviewSummaryText.Text = device == null
+ ? "Select an IED in the persistent Explorer."
+ : $"Current {current} · PASS {pass} · REVIEW {review} · FAIL {fail} · UNTESTED {untested} · historical {historical}";
+ }
+
+ if (_nativeFatPersistenceText != null)
+ {
+ _nativeFatPersistenceText.Text = _nativeFatCurrentState == null
+ ? "Per-IED JSON · non-destructive reconciliation"
+ : $"Resume file: {Path.GetFileName(_nativeFatCurrentState.StoragePath)}\nHistorical IEC identities remain stored when engineering changes.";
+ }
+
+ if (!_nativeFatLoading && device != null && _nativeFatStatusText != null &&
+ (_nativeFatStatusText.Text.StartsWith("Select", StringComparison.OrdinalIgnoreCase) ||
+ _nativeFatStatusText.Text.StartsWith("Loading", StringComparison.OrdinalIgnoreCase)))
+ {
+ _nativeFatStatusText.Text = $"{current} current · {historical} historical · {untested} untested";
+ }
+ }
+
+ private void SetNativeFatStatus(string text)
+ {
+ if (_nativeFatStatusText != null)
+ _nativeFatStatusText.Text = text;
+ }
+
+ private static string StableNativeFatDeviceKey(Iec61850MonitorDevice device)
+ => !string.IsNullOrWhiteSpace(device.DeviceId) ? device.DeviceId : device.Name;
+
+ private Brush ResourceBrush(string key, Color fallback)
+ => TryFindResource(key) as Brush ?? new SolidColorBrush(fallback);
+
+ private void NativeFat_MainWindowClosed(object? sender, EventArgs e)
+ {
+ _nativeFatInstallRetry?.Stop();
+ _nativeFatReconcileTimer?.Stop();
+ _nativeFatSaveTimer?.Stop();
+ _nativeFatLoadCts?.Cancel();
+ _nativeFatLoadCts?.Dispose();
+
+ PropertyChanged -= NativeFat_MainWindowPropertyChanged;
+ MainTabs.SelectionChanged -= NativeFat_MainTabsSelectionChanged;
+ SizeChanged -= NativeFat_MainWindowSizeChanged;
+ Closed -= NativeFat_MainWindowClosed;
+
+ if (_nativeFatObservedDevice != null)
+ {
+ _nativeFatObservedDevice.Signals.CollectionChanged -= NativeFatSignalCollectionChanged;
+ _nativeFatObservedDevice.Points.CollectionChanged -= NativeFatSignalCollectionChanged;
+ }
+
+ ClearNativeFatRows();
+ _nativeFatSaveGate.Dispose();
+ }
+}
diff --git a/MainWindow.ProductionFatEngineeringBootstrap.cs b/MainWindow.ProductionFatEngineeringBootstrap.cs
new file mode 100644
index 000000000..f7c8dfb36
--- /dev/null
+++ b/MainWindow.ProductionFatEngineeringBootstrap.cs
@@ -0,0 +1,194 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Threading;
+using ArIED61850Tester.Services.IoTesting;
+
+namespace ArIED61850Tester;
+
+///
+/// Makes the embedded production FAT workspace a projection of the Engineering workspace.
+/// Opening SCL in Explorer is therefore sufficient: selecting FAT reuses the already-parsed
+/// ARIEC SCL/static DataSet authority and the existing Engineering acquisition session.
+///
+public partial class MainWindow
+{
+ private bool _productionFatEngineeringBootstrapInstalled;
+ private bool _productionFatEngineeringBootstrapBusy;
+ private CancellationTokenSource? _productionFatEngineeringBootstrapCts;
+
+ [ModuleInitializer]
+ internal static void RegisterProductionFatEngineeringBootstrap()
+ {
+ EventManager.RegisterClassHandler(
+ typeof(MainWindow),
+ FrameworkElement.LoadedEvent,
+ new RoutedEventHandler(ProductionFatEngineeringBootstrap_Loaded),
+ handledEventsToo: true);
+ }
+
+ private static void ProductionFatEngineeringBootstrap_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is not MainWindow window || window._productionFatEngineeringBootstrapInstalled)
+ return;
+
+ window._productionFatEngineeringBootstrapInstalled = true;
+ window.MainTabs.SelectionChanged += window.ProductionFatEngineeringBootstrap_SelectionChanged;
+ window.PropertyChanged += window.ProductionFatEngineeringBootstrap_PropertyChanged;
+ window.Closed += window.ProductionFatEngineeringBootstrap_Closed;
+ window.QueueProductionFatEngineeringBootstrap();
+ }
+
+ private void ProductionFatEngineeringBootstrap_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (!ReferenceEquals(e.Source, MainTabs) || MainTabs.SelectedIndex != NativeFatWorkspaceIndex)
+ return;
+ QueueProductionFatEngineeringBootstrap();
+ }
+
+ private void ProductionFatEngineeringBootstrap_PropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName != nameof(SelectedDevice) || MainTabs.SelectedIndex != NativeFatWorkspaceIndex)
+ return;
+
+ // A running production FAT session owns its latched IED. The normal embedded-host
+ // selection bridge handles idle retargeting. Bootstrap is only needed before a FAT
+ // workspace exists.
+ if (_productionFatWindow == null && _loadedIoFatWindow == null)
+ QueueProductionFatEngineeringBootstrap();
+ }
+
+ private void QueueProductionFatEngineeringBootstrap()
+ {
+ if (!_productionFatEngineeringBootstrapInstalled || MainTabs.SelectedIndex != NativeFatWorkspaceIndex)
+ return;
+
+ Dispatcher.BeginInvoke(
+ DispatcherPriority.ContextIdle,
+ new Action(async () => await EnsureProductionFatFromEngineeringAsync()));
+ }
+
+ private async Task EnsureProductionFatFromEngineeringAsync()
+ {
+ if (_productionFatEngineeringBootstrapBusy ||
+ MainTabs.SelectedIndex != NativeFatWorkspaceIndex ||
+ !ProductionFatTabReady)
+ {
+ return;
+ }
+
+ if (_productionFatWindow is { IsLoaded: true } || _loadedIoFatWindow is { IsLoaded: true })
+ {
+ SynchronizeProductionFatSelectedIed();
+ return;
+ }
+
+ var selected = SelectedDevice;
+ if (selected?.SclWorkspace == null ||
+ selected.SclWorkspace.DesignModel.DataSets.Sum(dataSet => dataSet.Members.Count) == 0)
+ {
+ SetStatus(selected == null
+ ? "FAT · select an Engineering IED with a static DataSet."
+ : $"FAT · {selected.Name} has no static DataSet scope in the Engineering SCL model.");
+ return;
+ }
+
+ var engineeringDevices = Devices
+ .Where(device => device.SclWorkspace != null)
+ .Where(device => device.SclWorkspace!.DesignModel.DataSets.Sum(dataSet => dataSet.Members.Count) > 0)
+ .Where(device => !string.IsNullOrWhiteSpace(device.SclSourcePath))
+ .ToArray();
+ if (engineeringDevices.All(device => !ReferenceEquals(device, selected)))
+ {
+ SetStatus($"FAT · Engineering source provenance for {selected.Name} is unavailable; use Open SCL to restore the source authority.");
+ return;
+ }
+
+ _productionFatEngineeringBootstrapBusy = true;
+ _productionFatEngineeringBootstrapCts?.Cancel();
+ _productionFatEngineeringBootstrapCts?.Dispose();
+ _productionFatEngineeringBootstrapCts = CancellationTokenSource.CreateLinkedTokenSource(_applicationCancellation.Token);
+ var token = _productionFatEngineeringBootstrapCts.Token;
+ SetStatus($"FAT · preparing {selected.Name} from the Engineering static DataSet…");
+
+ try
+ {
+ var projection = await IoFatEngineeringWorkspaceProjectionService.BuildAsync(
+ engineeringDevices,
+ token);
+ token.ThrowIfCancellationRequested();
+
+ // Register the exact same ARIEC workspace instances already owned by Explorer.
+ // Production FAT preparation can therefore prove shared SCL authority without
+ // reparsing XML or starting a second model/acquisition stack.
+ _ioFatSclProjectImportService.AdoptEngineeringRuntimeWorkspaces(projection.RuntimeWorkspaces);
+
+ var launch = await IoTestWorkspaceBootstrapService.OpenSourcesAsync(
+ projection.Project,
+ projection.SourceInputs,
+ IoTestingProjectsRoot(),
+ IoTestingEvidenceRoot(),
+ CreateIoTestSession,
+ token);
+ token.ThrowIfCancellationRequested();
+
+ SynchronizeImportedSclFatWithEngineering(launch.Project);
+
+ // This automatic entry path is explicitly Static DataSet FAT. Engineering may
+ // also expose selected scalar aliases outside the DataSet, and older saved P2
+ // projects may contain scl-manual-* rows created from those aliases. Keep such
+ // rows/evidence in the project for audit continuity, but do not arm them in the
+ // shared workspace here. Otherwise a static member and its scalar alias can both
+ // resolve to the same live primary leaf and correctly trip session preflight.
+ var retiredManualRows = launch.Project.Ieds.Sum(
+ IoFatEngineeringSelectionBridge.RetireManualWorkspaceRowsForStaticDataSetMode);
+ if (retiredManualRows > 0)
+ {
+ AddLog(
+ "INFO",
+ "FAT",
+ $"Automatic Static DataSet scope retired {retiredManualRows} manual SCL workspace overlay(s); static membership remains authoritative.");
+ }
+
+ RegisterSharedSclSourcePaths(launch.Project, launch.Project.Ieds, projection.SourceInputs);
+ foreach (var ied in launch.Project.Ieds)
+ {
+ var device = ResolveIoTestDevice(ied.LiveDeviceId)
+ ?? ResolveIoTestDevice(ied.IpAddress)
+ ?? ResolveIoTestDevice(ied.IedName);
+ if (device is not null)
+ MarkSharedSelectionAuthority(device);
+ }
+
+ launch.Workspace.ScheduleSave();
+ await ShowIoTestingWorkspaceAsync(launch, importWarningCount: 0);
+ SynchronizeProductionFatSelectedIed();
+ SetStatus($"FAT ready · {selected.Name} · Engineering static DataSet authority reused · no SCL re-import.");
+ }
+ catch (OperationCanceledException)
+ {
+ // Fast navigation/close is normal. No modal interruption is appropriate here.
+ }
+ catch (Exception ex) when (ex is IOException or JsonException or InvalidDataException or UnauthorizedAccessException or ArgumentException or InvalidOperationException)
+ {
+ AddLog("WARN", "FAT", $"Automatic Engineering FAT bootstrap unavailable: {ex.Message}");
+ SetStatus($"FAT · could not reuse the Engineering static DataSet automatically: {ex.Message}");
+ }
+ finally
+ {
+ _productionFatEngineeringBootstrapBusy = false;
+ }
+ }
+
+ private void ProductionFatEngineeringBootstrap_Closed(object? sender, EventArgs e)
+ {
+ MainTabs.SelectionChanged -= ProductionFatEngineeringBootstrap_SelectionChanged;
+ PropertyChanged -= ProductionFatEngineeringBootstrap_PropertyChanged;
+ Closed -= ProductionFatEngineeringBootstrap_Closed;
+ _productionFatEngineeringBootstrapCts?.Cancel();
+ _productionFatEngineeringBootstrapCts?.Dispose();
+ _productionFatEngineeringBootstrapCts = null;
+ }
+}
diff --git a/MainWindow.ProductionFatNavigationParity.cs b/MainWindow.ProductionFatNavigationParity.cs
new file mode 100644
index 000000000..61787678d
--- /dev/null
+++ b/MainWindow.ProductionFatNavigationParity.cs
@@ -0,0 +1,173 @@
+using System.Runtime.CompilerServices;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+using System.Windows.Threading;
+
+namespace ArIED61850Tester;
+
+///
+/// Extends the existing six-destination responsive nav contract to the dynamically-added
+/// production FAT destination. The legacy MainWindow navigation code clamps index 6 to 5;
+/// this late correction keeps Diagnostics from remaining highlighted while FAT is active
+/// and moves the same selection pill into the seventh equal-width cell.
+///
+internal static class MainWindowProductionFatNavigationParity
+{
+ private static readonly string[] NavigationButtonNames =
+ [
+ "NavExplorerButton",
+ "NavLiveButton",
+ "NavEventsButton",
+ "NavAlarmButton",
+ "NavGooseButton",
+ "NavDiagnosticsButton",
+ "NavNativeFatButton"
+ ];
+
+ [ModuleInitializer]
+ internal static void Register()
+ {
+ EventManager.RegisterClassHandler(
+ typeof(MainWindow),
+ FrameworkElement.LoadedEvent,
+ new RoutedEventHandler(OnLoaded),
+ handledEventsToo: true);
+ EventManager.RegisterClassHandler(
+ typeof(MainWindow),
+ Button.ClickEvent,
+ new RoutedEventHandler(OnButtonClick),
+ handledEventsToo: true);
+ }
+
+ private static void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is not MainWindow window)
+ return;
+
+ window.SizeChanged -= Window_SizeChanged;
+ window.SizeChanged += Window_SizeChanged;
+ if (window.FindName("MainTabs") is TabControl tabs)
+ {
+ tabs.SelectionChanged -= Tabs_SelectionChanged;
+ tabs.SelectionChanged += Tabs_SelectionChanged;
+ }
+ Queue(window, animate: false);
+ }
+
+ private static void OnButtonClick(object sender, RoutedEventArgs e)
+ {
+ if (sender is not MainWindow window || e.Source is not Button button ||
+ !NavigationButtonNames.Contains(button.Name, StringComparer.Ordinal))
+ {
+ return;
+ }
+ Queue(window, animate: true);
+ }
+
+ private static void Tabs_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (sender is not TabControl tabs || !ReferenceEquals(e.Source, tabs) ||
+ Window.GetWindow(tabs) is not MainWindow window)
+ {
+ return;
+ }
+ Queue(window, animate: true);
+ }
+
+ private static void Window_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ if (sender is MainWindow window)
+ Queue(window, animate: false);
+ }
+
+ private static void Queue(MainWindow window, bool animate)
+ {
+ // Existing MainWindow + responsive-layout handlers still perform their historical
+ // six-slot correction. ApplicationIdle deliberately runs after those handlers so
+ // the seven-slot Engineering shell is the final visual authority.
+ window.Dispatcher.BeginInvoke(
+ DispatcherPriority.ApplicationIdle,
+ new Action(() => Apply(window, animate)));
+ }
+
+ private static void Apply(MainWindow window, bool animate)
+ {
+ if (window.FindName("MainTabs") is not TabControl tabs || tabs.Items.Count < 7 ||
+ window.FindName("WorkflowNavGrid") is not Grid grid ||
+ window.FindName("WorkflowNavShell") is not Border shell ||
+ window.FindName("WorkflowPill") is not Border pill)
+ {
+ return;
+ }
+
+ var fatButton = grid.Children
+ .OfType