From f436cccf50830559ee882059cabb55552cd04f00 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 04:23:28 +0700 Subject: [PATCH 01/16] fix(operator UX): render SNTP IPv4, responsive GOOSE timeline, safe all-publisher default and collapsed status sections --- Services/SntpNetworkRouteResolver.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Services/SntpNetworkRouteResolver.cs b/Services/SntpNetworkRouteResolver.cs index cea95840d..fa4e0392c 100644 --- a/Services/SntpNetworkRouteResolver.cs +++ b/Services/SntpNetworkRouteResolver.cs @@ -11,6 +11,11 @@ public sealed record SntpNetworkBinding( string InterfaceName, string InterfaceId) { + // The shared WPF ComboBox selection template may render the selected + // object via ToString rather than DisplayMemberPath. Never expose the + // internal record type where the operator expects a local IPv4 address. + public override string ToString() => LocalAddress.ToString(); + public string Summary => DirectedBroadcast == null ? $"{InterfaceName} • {LocalAddress}" : $"{InterfaceName} • {LocalAddress} → {DirectedBroadcast}"; From fdc396f676587d850646da457edcc6988ae70569 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 04:23:31 +0700 Subject: [PATCH 02/16] fix(operator UX): render SNTP IPv4, responsive GOOSE timeline, safe all-publisher default and collapsed status sections --- Views/GooseSubscriberLiteView.xaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Views/GooseSubscriberLiteView.xaml b/Views/GooseSubscriberLiteView.xaml index 3ea4030cb..417b8928d 100644 --- a/Views/GooseSubscriberLiteView.xaml +++ b/Views/GooseSubscriberLiteView.xaml @@ -1,7 +1,7 @@ + MinWidth="900" MinHeight="0"> @@ -410,9 +410,9 @@ BorderBrush="#DCE6F3" BorderThickness="1" CornerRadius="11" Padding="15" HorizontalAlignment="Center" VerticalAlignment="Center" MaxWidth="320"> - - From 1725072a865247a6354c8c92a8844e235b12fd2e Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 04:23:35 +0700 Subject: [PATCH 03/16] fix(operator UX): render SNTP IPv4, responsive GOOSE timeline, safe all-publisher default and collapsed status sections --- MainWindow.GooseIedScope.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/MainWindow.GooseIedScope.cs b/MainWindow.GooseIedScope.cs index 70114bb88..5983787e0 100644 --- a/MainWindow.GooseIedScope.cs +++ b/MainWindow.GooseIedScope.cs @@ -8,7 +8,7 @@ namespace ArIED61850Tester; public partial class MainWindow { // 0 = selected IED; 1 = all IEDs. Never filters the capture/decoder. - private int _gooseIedScopeIndex; + private int _gooseIedScopeIndex = 1; // Unresolved publishers must be visible at first capture. private ListCollectionView? _gooseEventScopeView; private ListCollectionView? _gooseStreamScopeView; @@ -27,6 +27,16 @@ public int GooseIedScopeIndex } } + // Keep the 500-event canonical timeline unchanged; this is only the + // presentation scope. A selected IED cannot claim unresolved Ethernet + // publisher identity solely from its MMS endpoint. + public string GooseEmptyEventTitle => GooseEvents.Count == 0 + ? "Waiting for the first meaningful GOOSE event" + : "No events match the selected IED"; + public string GooseEmptyEventDetail => GooseEvents.Count == 0 + ? "Frames are captured independently. New publishers and actual changes will appear here." + : $"Captured {GooseEvents.Count:N0} timeline event(s), but none has a verified publisher identity matching the selected IED. Choose All IED GOOSE signals to view them."; + public string GooseSelectedScopeHint => GooseIedScopeIndex == 1 ? "Display messages from every captured GOOSE publisher, including unresolved identities." : SelectedDevice is null @@ -58,6 +68,8 @@ private void RefreshGooseScopeViews() _gooseEventScopeView?.Refresh(); _gooseStreamScopeView?.Refresh(); Raise(nameof(GooseSelectedScopeHint)); + Raise(nameof(GooseEmptyEventTitle)); + Raise(nameof(GooseEmptyEventDetail)); _settingGooseLiveSelection = true; try { From d42620a173bae8114d2d294444390e851e2a4061 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 04:23:38 +0700 Subject: [PATCH 04/16] fix(operator UX): render SNTP IPv4, responsive GOOSE timeline, safe all-publisher default and collapsed status sections --- MainWindow.GooseTimeline.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MainWindow.GooseTimeline.cs b/MainWindow.GooseTimeline.cs index f115c3a8b..24552d05b 100644 --- a/MainWindow.GooseTimeline.cs +++ b/MainWindow.GooseTimeline.cs @@ -458,6 +458,8 @@ private void RaiseGoosePresentationState() { Raise(nameof(GoosePublisherCountText)); Raise(nameof(GooseEventCountText)); + Raise(nameof(GooseEmptyEventTitle)); + Raise(nameof(GooseEmptyEventDetail)); Raise(nameof(GooseSelectedLeafCountText)); Raise(nameof(GooseNoEventsVisibility)); } From 4fe547f2223ba11b5360bff434cc438da7456d0d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 04:23:41 +0700 Subject: [PATCH 05/16] fix(operator UX): render SNTP IPv4, responsive GOOSE timeline, safe all-publisher default and collapsed status sections --- Models/IedBrowserDetailSectionLayout.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Models/IedBrowserDetailSectionLayout.cs b/Models/IedBrowserDetailSectionLayout.cs index bef95664b..a14d6847c 100644 --- a/Models/IedBrowserDetailSectionLayout.cs +++ b/Models/IedBrowserDetailSectionLayout.cs @@ -58,7 +58,10 @@ public static IReadOnlyList Attach( result.Add(section with { LoadChildren = () => children, - ExpandByDefault = true + // Status summaries are rarely needed during routine browsing. + // Respect any explicit user toggle when the page is revisited. + ExpandByDefault = section.Name is not + ("Information received in last Report" or "Information received in last GOOSE") }); index = end - 1; } From 70d4ffa2b2c20b10ebbc18adc4bec01c0c470647 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 04:23:45 +0700 Subject: [PATCH 06/16] fix(operator UX): render SNTP IPv4, responsive GOOSE timeline, safe all-publisher default and collapsed status sections --- Models/IedBrowserPresentation.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Models/IedBrowserPresentation.cs b/Models/IedBrowserPresentation.cs index b9e4bfb14..1b40ab6d5 100644 --- a/Models/IedBrowserPresentation.cs +++ b/Models/IedBrowserPresentation.cs @@ -348,7 +348,8 @@ public sealed class IedBrowserDetailProjection { public BulkObservableCollection Rows { get; } = new(); - public void Reset(IReadOnlyList rows) + public void Reset(IReadOnlyList rows, + IReadOnlySet? expandedSectionNames = null) { // Attach UI-only collapsibility to exact section boundaries. Do not // mutate source projection rows or create a second evidence store. @@ -357,7 +358,8 @@ public void Reset(IReadOnlyList rows) foreach (var row in sections) { display.Add(row); - if (!row.ExpandByDefault || !row.HasChildren) continue; + if (!row.HasChildren || (expandedSectionNames is null + ? !row.ExpandByDefault : !expandedSectionNames.Contains(row.Name))) continue; row.IsExpanded = true; display.AddRange(row.GetChildren()); } From cbaf829710c285aaadc24359fb95d09a093c2ecc Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 04:24:45 +0700 Subject: [PATCH 07/16] fix(browser): preserve per-IED tree and per-page section expansion across navigation and refresh --- MainWindow.IedBrowser.cs | 60 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/MainWindow.IedBrowser.cs b/MainWindow.IedBrowser.cs index d96d7e9d0..ea270f868 100644 --- a/MainWindow.IedBrowser.cs +++ b/MainWindow.IedBrowser.cs @@ -21,6 +21,43 @@ public partial class MainWindow private readonly IedBrowserTreeProjection _iedBrowserTree = new(); private readonly IedBrowserDetailProjection _iedBrowserDetails = new(); private readonly IedBrowserNavigationTrail _iedBrowserNavigation = new(); + // Presentation preferences only. Never cache IEC values or MMS evidence. + // Exact DeviceId + stable page key, bounded to avoid accumulating transient IEDs. + private readonly Dictionary ExpandedKeys)> + _iedBrowserTreePreferences = new(StringComparer.Ordinal); + private readonly Dictionary> _iedBrowserSectionPreferences = + new(StringComparer.Ordinal); + private string? _iedBrowserRenderedSectionsKey; + + private void RememberIedBrowserTreePreferences() + { + if (_iedBrowserCachedDevice is null || _iedBrowserTree.Rows.Count == 0) + return; + var id = _iedBrowserCachedDevice.DeviceId; + if (_iedBrowserTreePreferences.Count >= 64 && + !_iedBrowserTreePreferences.ContainsKey(id)) + _iedBrowserTreePreferences.Remove(_iedBrowserTreePreferences.Keys.First()); + _iedBrowserTreePreferences[id] = ( + (IedBrowserTreeGrid.SelectedItem as IedBrowserTreeRow)?.Key, + _iedBrowserTree.ExpandedKeys()); + } + + private static string? BrowserSectionKey(Iec61850MonitorDevice? device, IedBrowserTreeRow? page) + => device is null || page is null ? null : device.DeviceId + "|" + page.Key; + + private void RememberIedBrowserSectionPreferences() + { + if (_iedBrowserRenderedSectionsKey is null || + !_iedBrowserDetails.Rows.Any(row => row.Kind == "Section" && row.HasChildren)) + return; + if (_iedBrowserSectionPreferences.Count >= 256 && + !_iedBrowserSectionPreferences.ContainsKey(_iedBrowserRenderedSectionsKey)) + _iedBrowserSectionPreferences.Remove(_iedBrowserSectionPreferences.Keys.First()); + _iedBrowserSectionPreferences[_iedBrowserRenderedSectionsKey] = + _iedBrowserDetails.Rows.Where(row => row.Kind == "Section" && + row.HasChildren && row.IsExpanded) + .Select(row => row.Name).ToHashSet(StringComparer.Ordinal); + } private bool _iedBrowserSyncingSelection; private string? _iedBrowserSelectedDoReference; private int _iedBrowserNavigationRenderTicket; @@ -2061,6 +2098,9 @@ private void RefreshIedBrowser() catch (IOException) { /* An unavailable source file cannot poison in-memory browsing. */ } catch (UnauthorizedAccessException) { /* Still show the cached/discovered model. */ } + // Save the previous IED's operator view before an asynchronous schema + // reset can erase its tree. Never snapshot an empty preparation placeholder. + RememberIedBrowserTreePreferences(); // Heavy schema indexing must never block the WPF selection dispatcher. if (!TryActivateIedBrowserSchema(device, model)) return; @@ -2078,12 +2118,13 @@ private void RefreshIedBrowser() var sameDevice = ReferenceEquals(device, _iedBrowserCachedDevice) && device.LiveValues.Association == _iedBrowserCachedAssociation && ReferenceEquals(model, _iedBrowserCachedModel); + _iedBrowserTreePreferences.TryGetValue(device.DeviceId, out var recalled); var priorKey = sameDevice ? (IedBrowserTreeGrid.SelectedItem as IedBrowserTreeRow)?.Key - : null; + : recalled.SelectedKey; var expandedKeys = sameDevice ? _iedBrowserTree.ExpandedKeys() - : new HashSet(StringComparer.Ordinal); + : recalled.ExpandedKeys ?? new HashSet(StringComparer.Ordinal); // Source inspection parses SCL XML; never perform it inside a click handler. if (!TryResolveIedBrowserSclInventory( @@ -2111,9 +2152,8 @@ private void RefreshIedBrowser() _iedBrowserRemoteFiles, EffectiveSclIedName(device), device.SclWorkspace?.DesignModel); _iedBrowserTree.Reset(roots, expandedKeys); - // Fresh IED/association/model epoch: all six Objects categories - // remain collapsed. Selecting Reports fills the right pane - // without materializing left LD/LN/RCB descendants. + // Restore only previously operator-expanded stable keys; no new + // branches are expanded automatically and no MMS reads are issued. IedBrowserTreeGrid.ItemsSource = _iedBrowserTree.Rows; var selected = priorKey == null ? null : _iedBrowserTree.FindVisible(priorKey); @@ -2326,7 +2366,14 @@ private void ReplaceIedBrowserDetails(IReadOnlyList rows) // Reset. A bound reset during routed selection can corrupt ItemInfo. IedBrowserDetailList.SelectedItem = null; IedBrowserDetailList.ItemsSource = null; - _iedBrowserDetails.Reset(rows); + RememberIedBrowserSectionPreferences(); + var nextKey = rows.Count == 0 ? null + : BrowserSectionKey(SelectedDevice, _iedBrowserRenderedPage); + var restored = nextKey is not null && + _iedBrowserSectionPreferences.TryGetValue(nextKey, out var expanded) + ? expanded : null; + _iedBrowserDetails.Reset(rows, restored); + _iedBrowserRenderedSectionsKey = nextKey; RebuildIedBrowserViewportIndex(); IedBrowserDetailList.ItemsSource = _iedBrowserDetails.Rows; } @@ -2342,6 +2389,7 @@ private void ToggleIedBrowserDetails(IedBrowserDetailRow row) IedBrowserDetailList.SelectedItem = null; IedBrowserDetailList.ItemsSource = null; _iedBrowserDetails.Toggle(row); + RememberIedBrowserSectionPreferences(); RebuildIedBrowserViewportIndex(); IedBrowserDetailList.ItemsSource = _iedBrowserDetails.Rows; _iedBrowserLastLiveVersion = -1; From 8691b09b16a710826bb2b62e5bebeb4e8da52899 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 04:25:16 +0700 Subject: [PATCH 08/16] fix(GOOSE layout): default collapsed shared Command Dock on passive capture and retain per-tab user override --- MainWindow.PersistentWorkbench.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/MainWindow.PersistentWorkbench.cs b/MainWindow.PersistentWorkbench.cs index 21139a39a..0ca46e9dc 100644 --- a/MainWindow.PersistentWorkbench.cs +++ b/MainWindow.PersistentWorkbench.cs @@ -43,6 +43,10 @@ private sealed class PersistentWorkbenchState // SV inspection is independent of MMS command admission. Default // collapsed, but retain the operator's explicit override per workspace. public bool SmvDockExpanded { get; set; } = false; + // Passive GOOSE observation gets the available timeline height by + // default. The operator can still expand MMS controls and the choice + // persists independently of the rest of the engineering workspaces. + public bool GooseDockExpanded { get; set; } = false; } [ModuleInitializer] @@ -386,6 +390,8 @@ private void CaptureP0CommandDockState(bool expanded) if (MainTabs.SelectedIndex == EngineeringNavigationRoute.SmvWorkspaceIndex) state.SmvDockExpanded = expanded; + else if (MainTabs.SelectedIndex == GooseSubscriberTabIndex) + state.GooseDockExpanded = expanded; else state.DockExpanded = expanded; } @@ -396,7 +402,10 @@ private void ApplyP0CommandDockState(PersistentWorkbenchState state, int workspa // MMS commands on passive SV inspection. Do not recreate the panel or // invoke any control/refresh operation as a navigation side effect. var desiredExpanded = workspaceIndex == EngineeringNavigationRoute.SmvWorkspaceIndex - ? state.SmvDockExpanded : state.DockExpanded; + ? state.SmvDockExpanded + : workspaceIndex == GooseSubscriberTabIndex + ? state.GooseDockExpanded + : state.DockExpanded; if (CommandPanelExpander.IsExpanded == desiredExpanded) return; From ecd3d735e72a93eec0f6beee7dcc465d05cad95b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 04:26:02 +0700 Subject: [PATCH 09/16] test(RC0 UX): GOOSE bounded timeline/filter and adaptive height, SNTP address text, per-IED browser state --- .../Rc0GooseSntpBrowserFieldUxTests.cs | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 tests/ARSAS.Tests/Rc0GooseSntpBrowserFieldUxTests.cs diff --git a/tests/ARSAS.Tests/Rc0GooseSntpBrowserFieldUxTests.cs b/tests/ARSAS.Tests/Rc0GooseSntpBrowserFieldUxTests.cs new file mode 100644 index 000000000..e2a888af1 --- /dev/null +++ b/tests/ARSAS.Tests/Rc0GooseSntpBrowserFieldUxTests.cs @@ -0,0 +1,107 @@ +using System.Net; +using System.Xml.Linq; +using ArIED61850Tester.Models; +using ArIED61850Tester.Services; + +namespace ARSAS.Tests; + +public sealed class Rc0GooseSntpBrowserFieldUxTests +{ + [Fact] + public void SntpPicker_AlwaysRendersIpv4AddressInsteadOfRecordType() + { + var address = IPAddress.Parse("192.168.1.175"); + var binding = new SntpNetworkBinding(address, IPAddress.Parse("255.255.255.0"), + null, "Ethernet", "GUID"); + Assert.Equal("192.168.1.175", binding.ToString()); + var header = Read("MainWindow.ClockSyncToggle.cs"); + Assert.Contains("DisplayMemberPath = nameof(SntpNetworkBinding.LocalAddress)", header); + } + + [Fact] + public void RcbAndGooseLastReceivedSections_DefaultCollapsedButRespectRestoredUserChoice() + { + var source = new IedBrowserDetailRow[] + { + new("Section", "Control Block attributes"), + new("Property", "Enabled", "Not read"), + new("Section", "Information received in last Report"), + new("Property", "Received frame count", "0"), + new("Section", "Data"), + new("DataHeader", "Name", "Value") + }; + var projection = new IedBrowserDetailProjection(); + projection.Reset(source); + var defaults = projection.Rows.Where(r => r.Kind == "Section").ToArray(); + Assert.True(defaults.Single(r => r.Name == "Control Block attributes").IsExpanded); + Assert.False(defaults.Single(r => r.Name == "Information received in last Report").IsExpanded); + projection.Reset(source, new HashSet(StringComparer.Ordinal) + { + "Information received in last Report" + }); + var restored = projection.Rows.Where(r => r.Kind == "Section").ToArray(); + Assert.False(restored.Single(r => r.Name == "Control Block attributes").IsExpanded); + Assert.True(restored.Single(r => r.Name == "Information received in last Report").IsExpanded); + + var goose = IedBrowserDetailSectionLayout.Attach( + new IedBrowserDetailRow[] + { + new("Section", "Information received in last GOOSE"), + new("Property", "Frame received at", "—"), + new("Section", "Data") + }); + Assert.False(goose.Single(r => r.Name == "Information received in last GOOSE").ExpandByDefault); + } + + [Fact] + public void GooseOperatorScope_StartsAllPublisherAndNeverForgesUnresolvedOwnership() + { + Assert.True(GooseIedScopePolicy.Matches(1, "SG7_BCU", "SG7_BCU", "Unresolved")); + Assert.False(GooseIedScopePolicy.Matches(0, "SG7_BCU", "SG7_BCU", "Unresolved")); + var source = Read("MainWindow.GooseIedScope.cs"); + Assert.Contains("private int _gooseIedScopeIndex = 1;", source); + Assert.Contains("none has a verified publisher identity", source); + Assert.Contains("_gooseEventScopeView?.Refresh()", source); + var ingress = Read("MainWindow.GooseTimeline.cs"); + Assert.Contains("MaxPendingGooseTimelineEvents = 512", ingress); + Assert.Contains("MaxGooseTimelineEvents = 500", ingress); + Assert.Contains("while (processed < 48", ingress); + } + + [Fact] + public void GooseTimeline_GrowsIntoAvailableViewportAndHasIndependentDockPreference() + { + var view = XDocument.Parse(Read("Views/GooseSubscriberLiteView.xaml")); + Assert.Equal("0", (string?)view.Root?.Attribute("MinHeight")); + var shell = Read("MainWindow.PersistentWorkbench.cs"); + Assert.Contains("GooseDockExpanded { get; set; } = false", shell); + Assert.Contains("workspaceIndex == GooseSubscriberTabIndex", shell); + Assert.Contains("state.GooseDockExpanded = expanded", shell); + Assert.Contains("state.GooseDockExpanded", shell); + Assert.Contains("GooseEmptyEventDetail", Read("Views/GooseSubscriberLiteView.xaml")); + } + + [Fact] + public void BrowserRetainsPerDeviceAndPerPageExpansionNotRemoteMmsObservations() + { + var src = Read("MainWindow.IedBrowser.cs"); + Assert.Contains("_iedBrowserTreePreferences", src); + Assert.Contains("_iedBrowserSectionPreferences", src); + Assert.Contains("RememberIedBrowserTreePreferences();", src); + Assert.Contains("RememberIedBrowserSectionPreferences();", src); + Assert.Contains("BrowserSectionKey(SelectedDevice, _iedBrowserRenderedPage)", src); + Assert.Contains("_iedBrowserDetails.Reset(rows, restored)", src); + Assert.Contains("recalled.ExpandedKeys", src); + Assert.DoesNotContain("BuildRcbExportRows(device, inventory, _iedBrowserSnapshot);\n _iedBrowserSectionPreferences", src); + } + + private static string Read(string path) + { + for (DirectoryInfo? dir = new(AppContext.BaseDirectory); dir is not null; dir = dir.Parent) + { + var full = Path.Combine(dir.FullName, path); + if (File.Exists(full)) return File.ReadAllText(full); + } + throw new FileNotFoundException(path); + } +} From c0f425e1e6d486b40de131d74331117bbb4411db Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 04:28:11 +0700 Subject: [PATCH 10/16] fix(GOOSE): actionable filtered-empty timeline to reveal all verified and unresolved publishers --- MainWindow.GooseIedScope.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MainWindow.GooseIedScope.cs b/MainWindow.GooseIedScope.cs index 5983787e0..c5e12d1da 100644 --- a/MainWindow.GooseIedScope.cs +++ b/MainWindow.GooseIedScope.cs @@ -33,6 +33,10 @@ public int GooseIedScopeIndex public string GooseEmptyEventTitle => GooseEvents.Count == 0 ? "Waiting for the first meaningful GOOSE event" : "No events match the selected IED"; + public System.Windows.Visibility GooseShowAllPublishersVisibility => + GooseIedScopeIndex == 0 && GooseEvents.Count > 0 && + !GooseVisibleEvents.Cast().Any() + ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed; public string GooseEmptyEventDetail => GooseEvents.Count == 0 ? "Frames are captured independently. New publishers and actual changes will appear here." : $"Captured {GooseEvents.Count:N0} timeline event(s), but none has a verified publisher identity matching the selected IED. Choose All IED GOOSE signals to view them."; @@ -70,6 +74,7 @@ private void RefreshGooseScopeViews() Raise(nameof(GooseSelectedScopeHint)); Raise(nameof(GooseEmptyEventTitle)); Raise(nameof(GooseEmptyEventDetail)); + Raise(nameof(GooseShowAllPublishersVisibility)); _settingGooseLiveSelection = true; try { From 1140c35346990ec7882e68c57a47ecb3bb50087c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 04:28:14 +0700 Subject: [PATCH 11/16] fix(GOOSE): actionable filtered-empty timeline to reveal all verified and unresolved publishers --- MainWindow.GooseTimeline.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/MainWindow.GooseTimeline.cs b/MainWindow.GooseTimeline.cs index 24552d05b..d17409433 100644 --- a/MainWindow.GooseTimeline.cs +++ b/MainWindow.GooseTimeline.cs @@ -460,6 +460,7 @@ private void RaiseGoosePresentationState() Raise(nameof(GooseEventCountText)); Raise(nameof(GooseEmptyEventTitle)); Raise(nameof(GooseEmptyEventDetail)); + Raise(nameof(GooseShowAllPublishersVisibility)); Raise(nameof(GooseSelectedLeafCountText)); Raise(nameof(GooseNoEventsVisibility)); } From 4e03e87ee861c0fbf274c333174c3eacca71023b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sun, 25 Oct 2026 04:28:18 +0700 Subject: [PATCH 12/16] fix(GOOSE): actionable filtered-empty timeline to reveal all verified and unresolved publishers --- Views/GooseSubscriberLiteView.xaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Views/GooseSubscriberLiteView.xaml b/Views/GooseSubscriberLiteView.xaml index 417b8928d..908afa007 100644 --- a/Views/GooseSubscriberLiteView.xaml +++ b/Views/GooseSubscriberLiteView.xaml @@ -415,6 +415,11 @@ +