Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
a214719
FAT M1: remove duplicate late navigation parity owner
masarray Sep 9, 2026
c6a856e
FAT M0: lock engineering authority and navigation regression contract
masarray Sep 9, 2026
ee1b83b
FAT M0: freeze Engineering workstation architecture contract
masarray Sep 9, 2026
661f359
FAT M1: add guarded canonical-shell migration
masarray Sep 9, 2026
d56fdab
FAT M1: run guarded canonical-shell migration
masarray Sep 9, 2026
4fd3d6f
FAT M1: make seven-destination shell canonical
github-actions[bot] Sep 9, 2026
7085d3f
FAT M1: keep navigation ownership in canonical MainWindow
masarray Sep 9, 2026
1710c52
FAT M1: lock canonical seven-destination shell ownership
masarray Sep 9, 2026
f5d0458
ci: guarded M1 seven-slot consistency fix
masarray Sep 10, 2026
f2df6d0
ci: fix guarded M1 migration newline matching
masarray Sep 10, 2026
b3058ce
fix: complete canonical seven-slot navigation ownership
github-actions[bot] Sep 10, 2026
acf76cd
docs: lock canonical seven-slot FAT navigation invariant
masarray Sep 10, 2026
1a0d3db
test: lock seven-cell responsive navigation geometry
masarray Sep 10, 2026
954795a
test: bind MainWindow navigation clamp to canonical FAT index
masarray Sep 10, 2026
4850435
chore: stage guarded M2 permanent FAT host migration
masarray Sep 10, 2026
b648186
chore: run guarded M2 permanent FAT host migration
masarray Sep 10, 2026
2c1a2ac
chore: correct guarded M2 launcher occurrence check
masarray Sep 10, 2026
523d8ab
chore: rerun guarded M2 migration with precise launcher target
masarray Sep 10, 2026
b885965
feat: complete M2 permanent production FAT host
github-actions[bot] Sep 10, 2026
581e3b4
docs: record M2 permanent FAT host verification contract
masarray Sep 10, 2026
cc0f855
test: make M2 permanent-host regression line-ending stable
masarray Sep 10, 2026
2f82570
test: align P0 no-flicker contract with M2 background prewarm
masarray Sep 10, 2026
6f67d92
chore: stage guarded M3 capture target latch migration
masarray Sep 10, 2026
a921fc8
chore: run guarded M3 capture target latch migration
masarray Sep 10, 2026
3d6a019
feat: start M3 selected IED binding with capture target latch
github-actions[bot] Sep 10, 2026
9556d6a
test: lock M3 viewed IED and capture target separation
masarray Sep 10, 2026
398b00f
chore: stage guarded M3 deterministic viewed IED binding
masarray Sep 10, 2026
ea3cb29
chore: run guarded M3 deterministic viewed IED binding migration
masarray Sep 10, 2026
ae0f0f3
feat: make M3 Engineering IED view binding deterministic
github-actions[bot] Sep 10, 2026
dd1343a
test: lock final M3 Explorer-to-FAT binding contract
masarray Sep 10, 2026
effc8f9
M3: infer production IED plan type for selected-device binding
masarray Sep 10, 2026
ec4b65c
chore: stage M3 persistence ownership guard
masarray Sep 10, 2026
e216eae
chore: run guarded M3 persistence migration
masarray Sep 10, 2026
b52843a
M3: fail closed on per-IED persistence restore
github-actions[bot] Sep 10, 2026
10dcbbc
M3: lock fail-closed per-IED restore contract
masarray Sep 10, 2026
8d1eb7e
M5: add DeviceId-aware persisted IED ownership guard
masarray Sep 10, 2026
73435c8
chore: stage guarded M5 persistence authority migration
masarray Sep 10, 2026
0c3e13d
chore: run guarded M5 persistence authority migration
masarray Sep 10, 2026
a9d6dd1
M5: bind persisted FAT ownership to Engineering DeviceId
github-actions[bot] Sep 10, 2026
4751621
M5: lock DeviceId ownership and read-only reconciliation
masarray Sep 10, 2026
f6ae335
docs: record M3 closure and M5 persistence invariants
masarray Sep 10, 2026
348c5b8
docs: clarify M5 reconciliation is pre-session and non-producing
masarray Sep 10, 2026
2fe9188
docs: mark M3 code and CI gate complete
masarray Sep 10, 2026
178b68d
M4: add fail-closed production FAT capture target adapter
masarray Sep 10, 2026
9ba4be8
chore: stage guarded M4 production adapter wiring
masarray Sep 10, 2026
9dcc6db
chore: run guarded M4 production adapter wiring
masarray Sep 10, 2026
2e37628
M4: add production capture lease and continue isolation regressions
masarray Sep 10, 2026
59d1d10
M4: latch production FAT target across prepare and continue
github-actions[bot] Sep 10, 2026
7a1570b
FAT M6: lock embedded production report preview parity
masarray Sep 10, 2026
8c0d2aa
FAT M7: remove obsolete native side-panel report preview
masarray Sep 10, 2026
4d8d773
FAT M7: guard single production preview authority
masarray Sep 10, 2026
bd6c083
FAT M7: remove obsolete native preview history inspector
masarray Sep 10, 2026
df62112
FAT M7: guard removal of obsolete preview inspector stack
masarray Sep 10, 2026
e373118
FAT M7: align M3 latch regression with production controller adapter
masarray Sep 10, 2026
025447a
FAT M7: keep cleanup guard on the current preflight authority
masarray Sep 10, 2026
aa11604
M7 retire duplicate native FAT workspace authority
masarray Sep 10, 2026
b65ccf9
M7 mount production FAT directly in canonical XAML slot
masarray Sep 10, 2026
b5a3421
M7 remove obsolete native FAT export authority
masarray Sep 10, 2026
a3972d5
M7 update canonical FAT shell regression contract
masarray Sep 10, 2026
166a6f6
M7 lock single production FAT authority regression
masarray Sep 10, 2026
1044d6e
M7 remove retired native FAT explorer reconciler
masarray Sep 10, 2026
8baacf5
M7 remove retired native FAT persistence safety runtime
masarray Sep 10, 2026
5269fed
M7 guard retired native FAT runtime fragments
masarray Sep 10, 2026
352883c
M7 align M6 preview regression with canonical FAT tab authority
masarray Sep 10, 2026
bf0b93b
fix(FAT): stop background bootstrap from mutating Engineering acquisi…
masarray Sep 10, 2026
f563aed
fix(FAT): preserve Engineering static DataSet authority
masarray Sep 10, 2026
f3c9f30
fix(FAT): normalize hidden donor before WPF Show and fail visibly
masarray Sep 10, 2026
883ec4a
test(FAT): cover field regression for static authority and WPF donor …
masarray Sep 10, 2026
fac9588
perf(FAT): keep static authority reassertion passive
masarray Sep 10, 2026
9d2baeb
Keep embedded Static DataSet FAT off the legacy 250ms polling path
masarray Sep 10, 2026
9275a54
Add regression coverage for embedded FAT polling isolation
masarray Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 67 additions & 19 deletions IoListTestingWindow.EmbeddedEngineeringHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ internal static void RegisterEmbeddedEngineeringFatHost()
handledEventsToo: true);
}

internal void PrepareForEmbeddedEngineeringHost()
{
// WPF refuses Window.Show() when ShowActivated=false while WindowState=Maximized.
// The production FAT XAML historically starts maximized, therefore normalize the
// invisible donor BEFORE Show(). Its exact center is re-parented into MainWindow on
// Loaded; the donor itself never needs a maximized native HWND.
WindowState = WindowState.Normal;
ShowActivated = false;
ShowInTaskbar = false;
Opacity = 0d;
WindowStartupLocation = WindowStartupLocation.Manual;
Left = -32000d;
Top = -32000d;
}

private static void EmbeddedEngineeringFatHost_Loaded(object sender, RoutedEventArgs e)
{
if (sender is not IoListTestingWindow window ||
Expand All @@ -45,9 +60,7 @@ window.Owner is not MainWindow owner ||
// 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.PrepareForEmbeddedEngineeringHost();

window.Dispatcher.BeginInvoke(
DispatcherPriority.Loaded,
Expand All @@ -64,24 +77,30 @@ private void TryMountIntoEngineering(MainWindow owner)
{
EnsureProductionFatPresentationForEmbeddedHost();
DisableLegacyEmbeddedCommandPanel();
var surface = DetachProductionFatCentralWorkspace();
if (surface == null)
return;
var surface = DetachProductionFatCentralWorkspace()
?? throw new InvalidOperationException("Production FAT center could not be detached from its donor window.");

_engineeringEmbeddedSurface = surface;
_engineeringEmbeddedMounted = owner.MountProductionFatWorkspace(this, surface);
if (!_engineeringEmbeddedMounted)
return;
throw new InvalidOperationException("Engineering FAT host rejected the production workspace surface.");

// 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)
{
// Never leave an invisible off-screen donor as a silent blank FAT failure.
// Restore the historical standalone presentation so the operator has a usable
// production FAT surface and a visible diagnostic if embedding itself fails.
WindowState = WindowState.Maximized;
Opacity = 1d;
ShowInTaskbar = true;
ShowActivated = true;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
Left = double.NaN;
Top = double.NaN;
MessageBox.Show(
owner,
$"ARSAS could not embed the production FAT workspace. The standalone FAT window will remain available.\n\n{ex.Message}",
Expand Down Expand Up @@ -190,22 +209,51 @@ private void DisableLegacyEmbeddedCommandPanel()

internal void SelectEngineeringDeviceForEmbeddedFat(Iec61850MonitorDevice? device)
{
if (!_engineeringEmbeddedMounted || device == null)
if (!_engineeringEmbeddedMounted)
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))
// M3 viewed-device contract: the persistent Engineering IED Explorer owns
// what the FAT grid displays. SelectedIed/Session.SelectContext changes only that
// projection; an active capture remains latched inside its per-IED controller.
// Clearing the Explorer selection or selecting an IED outside this FAT projection
// must clear the FAT view instead of silently leaving the previous IED on screen.
if (device == null)
{
if (CanSelectIed)
SelectedIed = null;
return;
}

// Resolve by strongest Engineering identity first. Do not use one OR predicate:
// a weak fallback on an earlier project row must never beat an exact live DeviceId.
var match = Project.Ieds.FirstOrDefault(_ => false);
if (!string.IsNullOrWhiteSpace(device.DeviceId))
{
match = Project.Ieds.FirstOrDefault(ied =>
!string.IsNullOrWhiteSpace(ied.LiveDeviceId) &&
ied.LiveDeviceId.Equals(device.DeviceId, StringComparison.OrdinalIgnoreCase));
}

if (match == null && !string.IsNullOrWhiteSpace(device.SclIedName))
{
match = Project.Ieds.FirstOrDefault(ied =>
ied.IedName.Equals(device.SclIedName, StringComparison.OrdinalIgnoreCase));
}

if (match == null && !string.IsNullOrWhiteSpace(device.Name))
{
match = Project.Ieds.FirstOrDefault(ied =>
ied.IedName.Equals(device.Name, StringComparison.OrdinalIgnoreCase));
}

if (match == null && !string.IsNullOrWhiteSpace(device.IpAddress))
{
match = Project.Ieds.FirstOrDefault(ied =>
!string.IsNullOrWhiteSpace(ied.IpAddress) &&
ied.IpAddress.Equals(device.IpAddress, StringComparison.OrdinalIgnoreCase));
}

// Do not retarget an active production FAT transaction/session. When idle, the
// persistent Engineering IED Explorer is the navigation/selection authority.
if (!CanSelectIed)
if (ReferenceEquals(SelectedIed, match) || !CanSelectIed)
return;

SelectedIed = match;
Expand Down
38 changes: 28 additions & 10 deletions IoListTestingWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,28 @@ private void SetAddingFatIeds(bool value)

private async void StartSession_Click(object sender, RoutedEventArgs e)
{
var selectedIed = SelectedIed;
if (selectedIed?.IsPreparing == true)
// M4 freezes both the IED owner and exact evidence scope before any asynchronous
// Engineering preparation. Explorer navigation may continue, but it cannot redirect
// this transaction or silently widen the production capture scope.
var requestedIed = SelectedIed;
if (requestedIed?.IsPreparing == true)
return;

var preflight = IoTestSessionPreflight.Validate(selectedIed);
if (!preflight.Succeeded)
IoFatCaptureTargetLease captureLease;
try
{
captureLease = IoFatProductionControllerAdapter.LatchStartTarget(Project, requestedIed);
}
catch (InvalidOperationException ex)
{
ShowActionResult(preflight, "FAT session scope is not ready");
ShowActionResult(
IoTestSessionActionResult.Failure(ex.Message),
"FAT session scope is not ready");
return;
}

PreparationStatusText = $"Connecting {selectedIed!.IedName} · {selectedIed.IpAddress}:102";
var selectedIed = captureLease.Ied;
PreparationStatusText = $"Connecting {selectedIed.IedName} · {selectedIed.IpAddress}:102";
RaisePreparationProperties();
try
{
Expand All @@ -155,7 +165,10 @@ private async void StartSession_Click(object sender, RoutedEventArgs e)
}
}

var result = Session.Start(selectedIed);
// Revalidate the frozen identity/configuration only after Engineering has
// completed connection preparation. The adapter delegates to the unchanged
// production session controller, which remains the sole evidence writer.
var result = IoFatProductionControllerAdapter.StartLatched(Project, Session, captureLease);
ShowActionResult(result, "FAT evidence session could not start");
RaiseStatusProperties();
if (result.Succeeded)
Expand Down Expand Up @@ -187,23 +200,28 @@ private async void StartSession_Click(object sender, RoutedEventArgs e)

private void PauseSession_Click(object sender, RoutedEventArgs e)
{
var result = Session.Pause();
var targetIed = SelectedIed;
var result = Session.Pause(targetIed);
ShowActionResult(result, "FAT session could not pause");
if (result.Succeeded)
Storage?.SaveNow();
}

private void ResumeSession_Click(object sender, RoutedEventArgs e)
{
var result = Session.Resume();
// Continue is explicitly IED-targeted. A later Explorer change cannot make the
// production controller rebind this active evidence session to another device.
var targetIed = SelectedIed;
var result = Session.Resume(targetIed);
ShowActionResult(result, "FAT session could not resume");
if (result.Succeeded)
Storage?.ScheduleSave();
}

private void StopSession_Click(object sender, RoutedEventArgs e)
{
var result = Session.Stop();
var targetIed = SelectedIed;
var result = Session.Stop(targetIed);
ShowActionResult(result, "FAT session could not stop");
if (result.Succeeded)
Storage?.SaveNow();
Expand Down
5 changes: 4 additions & 1 deletion MainWindow.IoTesting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,8 @@ private Task ShowIoTestingWorkspaceAsync(IoTestWorkspaceLaunchResult launch, int
var controller = launch.Session;
var persistence = launch.Workspace;
var window = new IoListTestingWindow(launch.Project, controller, persistence) { Owner = this };
if (ProductionFatTabReady)
window.PrepareForEmbeddedEngineeringHost();
RegisterLoadedIoFatWindow(window);
_activeIoTestSessionController = controller;
Interlocked.Exchange(ref _ioTestObservationSequence, DateTime.UtcNow.Ticks);
Expand Down Expand Up @@ -525,7 +527,8 @@ void WindowClosed(object? sender, EventArgs args)
}

window.Closed += WindowClosed;
Hide();
if (!ProductionFatTabReady)
Hide();
window.Show();
return Task.CompletedTask;
}
Expand Down
Loading
Loading