From db2a47cfac5976fa670337ea653d2cc706c68626 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:43:57 +0700 Subject: [PATCH 01/27] feat(report): persist selected static DataSet route across report-service attach --- apps/ied_simulator/src/MmsReportController.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/ied_simulator/src/MmsReportController.hpp b/apps/ied_simulator/src/MmsReportController.hpp index 82062e050..0fea55c52 100644 --- a/apps/ied_simulator/src/MmsReportController.hpp +++ b/apps/ied_simulator/src/MmsReportController.hpp @@ -34,6 +34,7 @@ class MmsReportController : public QObject { Q_PROPERTY(QStringList ownedDynamicDataSets READ ownedDynamicDataSets NOTIFY inventoryChanged) Q_PROPERTY(int selectedRcbIndex READ selectedRcbIndex NOTIFY selectionChanged) Q_PROPERTY(int selectedDataSetIndex READ selectedDataSetIndex NOTIFY selectionChanged) + Q_PROPERTY(QString staticRouteDataSet READ staticRouteDataSet NOTIFY selectionChanged) Q_PROPERTY(QVariantMap selectedRcb READ selectedRcb NOTIFY selectionChanged) Q_PROPERTY(QStringList selectedDataSetMembers READ selectedDataSetMembers NOTIFY selectionChanged) Q_PROPERTY(QVariantList receivedReports READ receivedReports NOTIFY reportsChanged) @@ -66,6 +67,7 @@ class MmsReportController : public QObject { [[nodiscard]] QStringList ownedDynamicDataSets() const { return ownedDynamicDataSets_; } [[nodiscard]] int selectedRcbIndex() const noexcept { return selectedRcbIndex_; } [[nodiscard]] int selectedDataSetIndex() const noexcept { return selectedDataSetIndex_; } + [[nodiscard]] QString staticRouteDataSet() const { return pendingStaticDataSetReference_; } [[nodiscard]] QVariantMap selectedRcb() const { return selectedRcb_; } [[nodiscard]] QStringList selectedDataSetMembers() const { return selectedDataSetMembers_; } [[nodiscard]] QVariantList receivedReports() const { return receivedReports_; } @@ -123,6 +125,7 @@ class MmsReportController : public QObject { void clearInventory(); void adoptEngineeringInventory(); void refreshSelection(); + bool selectStaticRcbForReference(const QString& reference, bool requireLiveProbe); void schedulePoll(); [[nodiscard]] std::shared_ptr replaceStopSource(); @@ -145,6 +148,7 @@ class MmsReportController : public QObject { int selectedDataSetIndex_{-1}; QVariantMap selectedRcb_; QStringList selectedDataSetMembers_; + QString pendingStaticDataSetReference_; QVariantList receivedReports_; QStringList events_; QStringList diagnostics_; From f7113e658961be8f18d101b8e6df9e9fd9dfe3c7 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:44:01 +0700 Subject: [PATCH 02/27] fix(report): retain exact static DataSet route and restore verified RCB after live attach --- .../ied_simulator/src/MmsReportController.cpp | 68 ++++++++++++++++--- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/apps/ied_simulator/src/MmsReportController.cpp b/apps/ied_simulator/src/MmsReportController.cpp index cc90bc7f1..c12984545 100644 --- a/apps/ied_simulator/src/MmsReportController.cpp +++ b/apps/ied_simulator/src/MmsReportController.cpp @@ -549,6 +549,9 @@ void MmsReportController::setEngineeringContext(IedEngineeringContextController* if (engineeringContext_ == value) return; if (engineeringContext_) disconnect(engineeringContext_, nullptr, this, nullptr); engineeringContext_ = value; + // A route belongs to one engineering authority only. Never carry a + // DataSet selection across a different IED/context pointer. + pendingStaticDataSetReference_.clear(); if (engineeringContext_) { connect( engineeringContext_, @@ -669,6 +672,10 @@ void MmsReportController::adoptEngineeringInventory() { associationProfile_.clear(); selectedRcbIndex_ = reportControls_.isEmpty() ? -1 : 0; selectedDataSetIndex_ = dataSets_.isEmpty() ? -1 : 0; + if (!pendingStaticDataSetReference_.isEmpty() && + !selectStaticRcbForReference(pendingStaticDataSetReference_, false)) { + selectedRcbIndex_ = -1; + } receivedReports_.clear(); events_.clear(); receivedReportCount_ = 0; @@ -778,9 +785,22 @@ bool MmsReportController::connectToIed() { self->state_ = State::ready; self->operationBusy_ = false; self->lastError_.clear(); - if (!self->reportControls_.isEmpty()) self->selectedRcbIndex_ = 0; - else if (!self->dataSets_.isEmpty()) self->selectedDataSetIndex_ = 0; - self->refreshSelection(); + if (!self->pendingStaticDataSetReference_.isEmpty()) { + if (!self->selectStaticRcbForReference( + self->pendingStaticDataSetReference_, true)) { + self->selectedRcbIndex_ = -1; + self->lastError_ = QStringLiteral( + "Selected static DataSet has no verified live RCB binding; " + "reporting remains disabled."); + self->appendDiagnostic(self->lastError_); + } + } else if (!self->reportControls_.isEmpty()) { + self->selectedRcbIndex_ = 0; + self->refreshSelection(); + } else if (!self->dataSets_.isEmpty()) { + self->selectedDataSetIndex_ = 0; + self->refreshSelection(); + } self->appendDiagnostic(QStringLiteral("Report service attach complete · %1 DataSet · %2 RCB") .arg(self->dataSets_.size()).arg(self->reportControls_.size())); emit self->inventoryChanged(); @@ -890,6 +910,7 @@ void MmsReportController::refreshSelection() { bool MmsReportController::selectRcb(const int row) { if (row < 0 || row >= reportControls_.size()) return false; + pendingStaticDataSetReference_.clear(); selectedRcbIndex_ = row; refreshSelection(); return true; @@ -903,12 +924,24 @@ bool MmsReportController::selectDataSet(const int row) { return true; } -bool MmsReportController::selectStaticRcbForDataSet(const int row) { - if (row < 0 || row >= dataSets_.size()) return false; - const auto dataSet = dataSets_.at(row).toMap(); - const auto reference = dataSet.value(QStringLiteral("reference")).toString(); - // A browse pivot is not authorization to modify a dynamic/foreign DataSet. - if (reference.isEmpty() || +bool MmsReportController::selectStaticRcbForReference( + const QString& reference, + const bool requireLiveProbe) { + if (reference.trimmed().isEmpty()) return false; + + int dataSetRow = -1; + QVariantMap dataSet; + for (int row = 0; row < dataSets_.size(); ++row) { + const auto candidate = dataSets_.at(row).toMap(); + if (!sameDataSetReference( + reference, candidate.value(QStringLiteral("reference")).toString())) { + continue; + } + dataSetRow = row; + dataSet = candidate; + break; + } + if (dataSetRow < 0 || !dataSet.value(QStringLiteral("directoryAvailable")).toBool() || dataSet.value(QStringLiteral("deletable")).toBool() || dataSet.value(QStringLiteral("dynamicOwned")).toBool() || @@ -916,14 +949,17 @@ bool MmsReportController::selectStaticRcbForDataSet(const int row) { dataSet.value(QStringLiteral("members")).toStringList().isEmpty()) { return false; } + for (int rcbRow = 0; rcbRow < reportControls_.size(); ++rcbRow) { const auto control = reportControls_.at(rcbRow).toMap(); const auto bound = control.value(QStringLiteral("dataSet")).toString(); - if (bound.isEmpty() || control.value(QStringLiteral("dynamicBinding")).toBool() || + if (bound.isEmpty() || + control.value(QStringLiteral("dynamicBinding")).toBool() || + (requireLiveProbe && !control.value(QStringLiteral("probeOk")).toBool()) || !sameDataSetReference(bound, reference)) { continue; } - // Selection changes only after a verified exact canonical match. + selectedDataSetIndex_ = dataSetRow; selectedRcbIndex_ = rcbRow; refreshSelection(); return true; @@ -931,6 +967,16 @@ bool MmsReportController::selectStaticRcbForDataSet(const int row) { return false; } +bool MmsReportController::selectStaticRcbForDataSet(const int row) { + if (row < 0 || row >= dataSets_.size()) return false; + const auto reference = dataSets_.at(row).toMap() + .value(QStringLiteral("reference")).toString(); + if (!selectStaticRcbForReference(reference, false)) return false; + pendingStaticDataSetReference_ = reference; + emit selectionChanged(); + return true; +} + bool MmsReportController::createDynamicDataSet( const QString& dataSetReference, const QVariantList& canonicalMembers) { From 5eb5d1d93a9063886b44077ac528308ea97570a2 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:44:24 +0700 Subject: [PATCH 03/27] test(report): preserve Dataset Signals static route across live report reattach --- .../src/ied_report_workspace_qa.cpp | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/apps/ied_simulator/src/ied_report_workspace_qa.cpp b/apps/ied_simulator/src/ied_report_workspace_qa.cpp index e8c6e19ec..b6241e7b8 100644 --- a/apps/ied_simulator/src/ied_report_workspace_qa.cpp +++ b/apps/ied_simulator/src/ied_report_workspace_qa.cpp @@ -337,16 +337,49 @@ int main(int argc, char* argv[]) { !reports.selectStaticRcbForDataSet(-1) && !reports.selectStaticRcbForDataSet(dataSets.size()) && reports.selectedRcbIndex() == matchedRcb; - if (!staticPivotValid || !invalidPivotRejected || !reports.selectRcb(selected) || + const auto staticRoute = reports.staticRouteDataSet(); + if (!staticPivotValid || !invalidPivotRejected || + staticRoute.isEmpty() || reports.selectedRcb().value(QStringLiteral("reference")).toString() != originalRcb) { qCritical() << "REPORTS_WORKBENCH_FAIL dataset_signals_static_rcb_pivot" - << staticDataSetRow << matchedRcb << originalRcb; + << staticDataSetRow << matchedRcb << originalRcb << staticRoute; return 29; } qInfo() << "DATASET_SIGNALS_STATIC_PIVOT_PASS" << "dataset=" << staticDataSetRow << "bound_rcb=" << matchedRcb << "read_only=true no_gi=true"; + // The user's Dataset Signals choice is made before/while auxiliary report + // service attachment in the Browser. Reconnect must restore that exact + // static route after live RCB probing instead of snapping to row 0. + reports.disconnectFromIed(); + if (!waitUntil([&reports] { return !reports.connected() && !reports.busy(); }, 8'000) || + !reports.connectToIed() || + !waitUntil([&reports] { return reports.connected() && !reports.busy(); }, 12'000) || + reports.staticRouteDataSet() != staticRoute || + reports.selectedRcb().value(QStringLiteral("reference")).toString() != originalRcb || + !reports.selectedRcb().value(QStringLiteral("probeOk")).toBool() || + !reports.selectedRcb().value(QStringLiteral("dataSet")).toString().contains( + reports.selectedDataSetMembers().isEmpty() + ? QStringLiteral("__missing__") + : QString{})) { + qCritical().noquote() << "REPORTS_WORKBENCH_FAIL static_route_restore" + << "route=" << reports.staticRouteDataSet() + << "rcb=" << reports.selectedRcb().value(QStringLiteral("reference")).toString() + << "probe=" << reports.selectedRcb().value(QStringLiteral("probeOk")).toBool() + << "error=" << reports.lastError(); + return 30; + } + if (!sameDataSetReference( + reports.selectedRcb().value(QStringLiteral("dataSet")).toString(), + staticRoute)) { + qCritical() << "REPORTS_WORKBENCH_FAIL static_route_dataset_changed"; + return 31; + } + qInfo() << "STATIC_REPORT_ROUTE_RESTORE_PASS" + << staticRoute << "rcb=" << reports.selectedRcb().value(QStringLiteral("reference")).toString() + << "live_probe=true"; + const int memberCount = reports.selectedDataSetMembers().size(); if (!reports.enableSelected(true) || From 9e7e7d38573c199a68c058a4f2fc2fc75f461374 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:44:33 +0700 Subject: [PATCH 04/27] test(report): require restored static route retains ordered members --- apps/ied_simulator/src/ied_report_workspace_qa.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/ied_simulator/src/ied_report_workspace_qa.cpp b/apps/ied_simulator/src/ied_report_workspace_qa.cpp index b6241e7b8..72bfa47b2 100644 --- a/apps/ied_simulator/src/ied_report_workspace_qa.cpp +++ b/apps/ied_simulator/src/ied_report_workspace_qa.cpp @@ -359,10 +359,7 @@ int main(int argc, char* argv[]) { reports.staticRouteDataSet() != staticRoute || reports.selectedRcb().value(QStringLiteral("reference")).toString() != originalRcb || !reports.selectedRcb().value(QStringLiteral("probeOk")).toBool() || - !reports.selectedRcb().value(QStringLiteral("dataSet")).toString().contains( - reports.selectedDataSetMembers().isEmpty() - ? QStringLiteral("__missing__") - : QString{})) { + reports.selectedDataSetMembers().isEmpty()) { qCritical().noquote() << "REPORTS_WORKBENCH_FAIL static_route_restore" << "route=" << reports.staticRouteDataSet() << "rcb=" << reports.selectedRcb().value(QStringLiteral("reference")).toString() From 6fe8991cd57721e8e5cd98c52a227e0be779c214 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:44:49 +0700 Subject: [PATCH 05/27] fix(report): clear armed static route when operator selects another DataSet --- apps/ied_simulator/src/MmsReportController.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/ied_simulator/src/MmsReportController.cpp b/apps/ied_simulator/src/MmsReportController.cpp index c12984545..c852d3710 100644 --- a/apps/ied_simulator/src/MmsReportController.cpp +++ b/apps/ied_simulator/src/MmsReportController.cpp @@ -918,8 +918,15 @@ bool MmsReportController::selectRcb(const int row) { bool MmsReportController::selectDataSet(const int row) { if (row < 0 || row >= dataSets_.size()) return false; + const auto reference = dataSets_.at(row).toMap() + .value(QStringLiteral("reference")).toString(); + if (!pendingStaticDataSetReference_.isEmpty() && + !sameDataSetReference(pendingStaticDataSetReference_, reference)) { + pendingStaticDataSetReference_.clear(); + } selectedDataSetIndex_ = row; - selectedDataSetMembers_ = dataSets_.at(row).toMap().value(QStringLiteral("members")).toStringList(); + selectedDataSetMembers_ = dataSets_.at(row).toMap() + .value(QStringLiteral("members")).toStringList(); emit selectionChanged(); return true; } From 3c7f82e330e9b3fe77627ebed91f249dd664d40b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:44:52 +0700 Subject: [PATCH 06/27] feat(browser): name the canonical DataSet route as explicit Static Reporting --- apps/ied_simulator/qml/BrowserDataSetPane.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ied_simulator/qml/BrowserDataSetPane.qml b/apps/ied_simulator/qml/BrowserDataSetPane.qml index 2a1213b1a..eafaef5c2 100644 --- a/apps/ied_simulator/qml/BrowserDataSetPane.qml +++ b/apps/ied_simulator/qml/BrowserDataSetPane.qml @@ -118,7 +118,7 @@ Rectangle { Item { Layout.fillWidth: true } ActionButton { theme: root.theme - text: "Inspect bound RCB…" + text: "Static Reporting…" enabled: root.selectedDataSet !== null && root.selectedDataSet.directoryAvailable === true && root.selectedDataSet.deletable !== true @@ -127,7 +127,7 @@ Rectangle { && reports.selectedDataSetMembers.length > 0 onClicked: root.inspectStaticReportRequested() ToolTip.visible: hovered - ToolTip.text: "Navigate to an RCB bound to this exact static DataSet. No write, enable or GI." + ToolTip.text: "Prepare this exact static DataSet in the shared Reports path. Selection only: no DatSet write, enable or GI." } ActionButton { theme: root.theme From d41558c763b91ba0635eaaeaefc0f8c010d23e27 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:44:55 +0700 Subject: [PATCH 07/27] feat(browser): surface exact static DataSet route before explicit Enable + GI --- apps/ied_simulator/qml/IedBrowserWorkspace.qml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apps/ied_simulator/qml/IedBrowserWorkspace.qml b/apps/ied_simulator/qml/IedBrowserWorkspace.qml index 4698153e0..e302a8208 100644 --- a/apps/ied_simulator/qml/IedBrowserWorkspace.qml +++ b/apps/ied_simulator/qml/IedBrowserWorkspace.qml @@ -617,6 +617,23 @@ Item { reports.dataSets[reports.selectedDataSetIndex]) } + Label { + visible: root.activeSection === 2 && reports.staticRouteDataSet.length > 0 + text: "STATIC · " + reports.staticRouteDataSet + color: root.theme.green + font.pixelSize: root.theme.captionSize + elide: Text.ElideMiddle + Layout.maximumWidth: 220 + ToolTip.visible: staticRouteMouse.containsMouse + ToolTip.text: "Exact canonical DataSet selected for report-only static reporting. Enable + GI remains explicit." + MouseArea { + id: staticRouteMouse + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + } + Button { visible: root.activeSection === 2 text: "Enable + GI" From 44073e2880862d507f6ac56efb9821eeb7f286e9 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:45:10 +0700 Subject: [PATCH 08/27] test(report): require exact canonical DataSet identity after route restore --- apps/ied_simulator/src/ied_report_workspace_qa.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/ied_simulator/src/ied_report_workspace_qa.cpp b/apps/ied_simulator/src/ied_report_workspace_qa.cpp index 72bfa47b2..eef449dda 100644 --- a/apps/ied_simulator/src/ied_report_workspace_qa.cpp +++ b/apps/ied_simulator/src/ied_report_workspace_qa.cpp @@ -367,9 +367,7 @@ int main(int argc, char* argv[]) { << "error=" << reports.lastError(); return 30; } - if (!sameDataSetReference( - reports.selectedRcb().value(QStringLiteral("dataSet")).toString(), - staticRoute)) { + if (reports.selectedRcb().value(QStringLiteral("dataSet")).toString() != staticRoute) { qCritical() << "REPORTS_WORKBENCH_FAIL static_route_dataset_changed"; return 31; } From e2211040068457dd52cdb18b9102c4163cf4ee35 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:45:18 +0700 Subject: [PATCH 09/27] test(browser): lock explicit Static Reporting route indicator contract --- apps/ied_simulator/test_browser_report_authoring.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/ied_simulator/test_browser_report_authoring.py b/apps/ied_simulator/test_browser_report_authoring.py index 485b67c43..0e5ffcabd 100644 --- a/apps/ied_simulator/test_browser_report_authoring.py +++ b/apps/ied_simulator/test_browser_report_authoring.py @@ -79,6 +79,8 @@ def require(text: str, token: str, label: str) -> None: "else signalCatalogDialog.close()", "function inspectBoundStaticRcb()", "reports.selectStaticRcbForDataSet(reports.selectedDataSetIndex)", + "reports.staticRouteDataSet.length > 0", + 'text: "STATIC · " + reports.staticRouteDataSet', "routeMessage: root.datasetRouteStatus", 'objectName: "iedBrowserSignalCatalog"', 'text: "Dataset Signals"', @@ -117,7 +119,7 @@ def require(text: str, token: str, label: str) -> None: "STATIC / READ-ONLY", "Membership is immutable", "signal inspectStaticReportRequested()", - 'text: "Inspect bound RCB…"', + 'text: "Static Reporting…"', "root.selectedDataSet.directoryAvailable === true", "root.selectedDataSet.deletable !== true", ): @@ -127,6 +129,7 @@ def require(text: str, token: str, label: str) -> None: "ownedDynamicDataSets", "enableSelectedAuthored", "selectStaticRcbForDataSet", + "staticRouteDataSet", "createDynamicDataSet", "deleteDynamicDataSet", ): From bc6be27220e4f5445bf570f3fb0f2b350f4f1c92 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:45:52 +0700 Subject: [PATCH 10/27] fix(report): clear stale canonical RCB when live static route cannot be verified --- apps/ied_simulator/src/MmsReportController.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/ied_simulator/src/MmsReportController.cpp b/apps/ied_simulator/src/MmsReportController.cpp index c852d3710..fa71f277a 100644 --- a/apps/ied_simulator/src/MmsReportController.cpp +++ b/apps/ied_simulator/src/MmsReportController.cpp @@ -789,6 +789,7 @@ bool MmsReportController::connectToIed() { if (!self->selectStaticRcbForReference( self->pendingStaticDataSetReference_, true)) { self->selectedRcbIndex_ = -1; + self->refreshSelection(); self->lastError_ = QStringLiteral( "Selected static DataSet has no verified live RCB binding; " "reporting remains disabled."); From b961623bd8269881a9c8fad07fea04f3ad0f28ab Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:46:55 +0700 Subject: [PATCH 11/27] fix(report): bind pending static route to canonical context fingerprint --- apps/ied_simulator/src/MmsReportController.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/ied_simulator/src/MmsReportController.hpp b/apps/ied_simulator/src/MmsReportController.hpp index 0fea55c52..5392cf909 100644 --- a/apps/ied_simulator/src/MmsReportController.hpp +++ b/apps/ied_simulator/src/MmsReportController.hpp @@ -149,6 +149,7 @@ class MmsReportController : public QObject { QVariantMap selectedRcb_; QStringList selectedDataSetMembers_; QString pendingStaticDataSetReference_; + QString pendingStaticContextFingerprint_; QVariantList receivedReports_; QStringList events_; QStringList diagnostics_; From 6e40609ad5c8558da71304cfae25b002ee48290e Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:46:59 +0700 Subject: [PATCH 12/27] fix(report): reject stale static route when canonical engineering source changes --- .../ied_simulator/src/MmsReportController.cpp | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/ied_simulator/src/MmsReportController.cpp b/apps/ied_simulator/src/MmsReportController.cpp index fa71f277a..cf02e283b 100644 --- a/apps/ied_simulator/src/MmsReportController.cpp +++ b/apps/ied_simulator/src/MmsReportController.cpp @@ -552,6 +552,7 @@ void MmsReportController::setEngineeringContext(IedEngineeringContextController* // A route belongs to one engineering authority only. Never carry a // DataSet selection across a different IED/context pointer. pendingStaticDataSetReference_.clear(); + pendingStaticContextFingerprint_.clear(); if (engineeringContext_) { connect( engineeringContext_, @@ -638,6 +639,14 @@ void MmsReportController::adoptEngineeringInventory() { const auto& canonical = *engineeringContext_->modelSnapshot(); const auto seed = buildContextDiscoverySeed(canonical); auto ui = buildDiscoveryUi(seed, QString{}); + if (!pendingStaticDataSetReference_.isEmpty() && + (pendingStaticContextFingerprint_.isEmpty() || + pendingStaticContextFingerprint_ != engineeringContext_->structuralFingerprint())) { + appendDiagnostic(QStringLiteral( + "Cleared stale static DataSet route after engineering context changed.")); + pendingStaticDataSetReference_.clear(); + pendingStaticContextFingerprint_.clear(); + } // An opened SCL model has no live RCB probe response yet. Preserve its // verified canonical DatSet binding for read-only Browser navigation. // Never mark the RCB as probed or writable based on file evidence alone. @@ -912,6 +921,7 @@ void MmsReportController::refreshSelection() { bool MmsReportController::selectRcb(const int row) { if (row < 0 || row >= reportControls_.size()) return false; pendingStaticDataSetReference_.clear(); + pendingStaticContextFingerprint_.clear(); selectedRcbIndex_ = row; refreshSelection(); return true; @@ -924,6 +934,7 @@ bool MmsReportController::selectDataSet(const int row) { if (!pendingStaticDataSetReference_.isEmpty() && !sameDataSetReference(pendingStaticDataSetReference_, reference)) { pendingStaticDataSetReference_.clear(); + pendingStaticContextFingerprint_.clear(); } selectedDataSetIndex_ = row; selectedDataSetMembers_ = dataSets_.at(row).toMap() @@ -979,8 +990,14 @@ bool MmsReportController::selectStaticRcbForDataSet(const int row) { if (row < 0 || row >= dataSets_.size()) return false; const auto reference = dataSets_.at(row).toMap() .value(QStringLiteral("reference")).toString(); - if (!selectStaticRcbForReference(reference, false)) return false; + if (!engineeringContext_ || !engineeringContext_->loaded() || + engineeringContext_->selectionRequired() || + engineeringContext_->structuralFingerprint().isEmpty() || + !selectStaticRcbForReference(reference, false)) { + return false; + } pendingStaticDataSetReference_ = reference; + pendingStaticContextFingerprint_ = engineeringContext_->structuralFingerprint(); emit selectionChanged(); return true; } From 2e2f842f0e59542496bfa0c8c899c86ed5b24b4c Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:47:25 +0700 Subject: [PATCH 13/27] refactor(report): derive static route authority from context or live endpoint --- apps/ied_simulator/src/MmsReportController.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/ied_simulator/src/MmsReportController.hpp b/apps/ied_simulator/src/MmsReportController.hpp index 5392cf909..2c34482ba 100644 --- a/apps/ied_simulator/src/MmsReportController.hpp +++ b/apps/ied_simulator/src/MmsReportController.hpp @@ -126,6 +126,7 @@ class MmsReportController : public QObject { void adoptEngineeringInventory(); void refreshSelection(); bool selectStaticRcbForReference(const QString& reference, bool requireLiveProbe); + [[nodiscard]] QString staticRouteAuthorityKey() const; void schedulePoll(); [[nodiscard]] std::shared_ptr replaceStopSource(); From ca380218c9b3315c2c1eff1322252dcf77fc3061 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:47:29 +0700 Subject: [PATCH 14/27] fix(report): preserve standalone live report QA while scoping route to endpoint authority --- .../ied_simulator/src/MmsReportController.cpp | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/apps/ied_simulator/src/MmsReportController.cpp b/apps/ied_simulator/src/MmsReportController.cpp index cf02e283b..eab916da2 100644 --- a/apps/ied_simulator/src/MmsReportController.cpp +++ b/apps/ied_simulator/src/MmsReportController.cpp @@ -536,12 +536,16 @@ void MmsReportController::setHost(const QString& value) { const auto normalized = value.trimmed(); if (host_ == normalized) return; host_ = normalized; + pendingStaticDataSetReference_.clear(); + pendingStaticContextFingerprint_.clear(); emit configurationChanged(); } void MmsReportController::setPort(const int value) { if (value < 1 || value > 65'535 || port_ == value) return; port_ = value; + pendingStaticDataSetReference_.clear(); + pendingStaticContextFingerprint_.clear(); emit configurationChanged(); } @@ -566,6 +570,17 @@ void MmsReportController::setEngineeringContext(IedEngineeringContextController* if (!connected() && !busy()) adoptEngineeringInventory(); } +QString MmsReportController::staticRouteAuthorityKey() const { + if (engineeringContext_ && engineeringContext_->loaded() && + !engineeringContext_->selectionRequired() && + !engineeringContext_->structuralFingerprint().isEmpty()) { + return QStringLiteral("context:") + engineeringContext_->structuralFingerprint(); + } + const auto host = host_.trimmed().toLower(); + if (host.isEmpty() || port_ < 1 || port_ > 65'535) return {}; + return QStringLiteral("endpoint:%1:%2").arg(host).arg(port_); +} + bool MmsReportController::connected() const noexcept { return state_ == State::ready || state_ == State::authoring || state_ == State::enabling || state_ == State::active || state_ == State::disabling || state_ == State::cleanup_required; @@ -641,7 +656,7 @@ void MmsReportController::adoptEngineeringInventory() { auto ui = buildDiscoveryUi(seed, QString{}); if (!pendingStaticDataSetReference_.isEmpty() && (pendingStaticContextFingerprint_.isEmpty() || - pendingStaticContextFingerprint_ != engineeringContext_->structuralFingerprint())) { + pendingStaticContextFingerprint_ != staticRouteAuthorityKey())) { appendDiagnostic(QStringLiteral( "Cleared stale static DataSet route after engineering context changed.")); pendingStaticDataSetReference_.clear(); @@ -990,14 +1005,13 @@ bool MmsReportController::selectStaticRcbForDataSet(const int row) { if (row < 0 || row >= dataSets_.size()) return false; const auto reference = dataSets_.at(row).toMap() .value(QStringLiteral("reference")).toString(); - if (!engineeringContext_ || !engineeringContext_->loaded() || - engineeringContext_->selectionRequired() || - engineeringContext_->structuralFingerprint().isEmpty() || + const auto authorityKey = staticRouteAuthorityKey(); + if (authorityKey.isEmpty() || !selectStaticRcbForReference(reference, false)) { return false; } pendingStaticDataSetReference_ = reference; - pendingStaticContextFingerprint_ = engineeringContext_->structuralFingerprint(); + pendingStaticContextFingerprint_ = authorityKey; emit selectionChanged(); return true; } From 60665ae7a35aa0d631a97180f1bf8d999595a69f Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:47:43 +0700 Subject: [PATCH 15/27] test(report): lock static route authority and fail-closed live restore contract --- apps/ied_simulator/test_browser_report_authoring.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/ied_simulator/test_browser_report_authoring.py b/apps/ied_simulator/test_browser_report_authoring.py index 0e5ffcabd..9358b31ad 100644 --- a/apps/ied_simulator/test_browser_report_authoring.py +++ b/apps/ied_simulator/test_browser_report_authoring.py @@ -38,6 +38,9 @@ def require(text: str, token: str, label: str) -> None: "Enable + GI requires general-interrogation in authored TrgOps", "Static/non-owned DataSet binding is immutable", "bool MmsReportController::selectStaticRcbForDataSet(", + "QString MmsReportController::staticRouteAuthorityKey() const", + "pendingStaticContextFingerprint_", + "Selected static DataSet has no verified live RCB binding", "sameDataSetReference(bound, reference)", 'dataSet.value(QStringLiteral("directoryAvailable")).toBool()', 'dataSet.value(QStringLiteral("deletable")).toBool()', From c934c79d8ef388989ab9ad10abaa195b13a6b64b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:47:56 +0700 Subject: [PATCH 16/27] fix(report): notify UI immediately when endpoint/context invalidates static route --- apps/ied_simulator/src/MmsReportController.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/ied_simulator/src/MmsReportController.cpp b/apps/ied_simulator/src/MmsReportController.cpp index eab916da2..08e960250 100644 --- a/apps/ied_simulator/src/MmsReportController.cpp +++ b/apps/ied_simulator/src/MmsReportController.cpp @@ -535,18 +535,22 @@ MmsReportController::~MmsReportController() { void MmsReportController::setHost(const QString& value) { const auto normalized = value.trimmed(); if (host_ == normalized) return; + const bool routeChanged = !pendingStaticDataSetReference_.isEmpty(); host_ = normalized; pendingStaticDataSetReference_.clear(); pendingStaticContextFingerprint_.clear(); emit configurationChanged(); + if (routeChanged) emit selectionChanged(); } void MmsReportController::setPort(const int value) { if (value < 1 || value > 65'535 || port_ == value) return; + const bool routeChanged = !pendingStaticDataSetReference_.isEmpty(); port_ = value; pendingStaticDataSetReference_.clear(); pendingStaticContextFingerprint_.clear(); emit configurationChanged(); + if (routeChanged) emit selectionChanged(); } void MmsReportController::setEngineeringContext(IedEngineeringContextController* value) { @@ -555,8 +559,10 @@ void MmsReportController::setEngineeringContext(IedEngineeringContextController* engineeringContext_ = value; // A route belongs to one engineering authority only. Never carry a // DataSet selection across a different IED/context pointer. + const bool routeChanged = !pendingStaticDataSetReference_.isEmpty(); pendingStaticDataSetReference_.clear(); pendingStaticContextFingerprint_.clear(); + if (routeChanged) emit selectionChanged(); if (engineeringContext_) { connect( engineeringContext_, From a31bd9982bbe27886737574a8fca5abe90ba937b Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:48:19 +0700 Subject: [PATCH 17/27] refactor(report): name persisted static route authority precisely --- apps/ied_simulator/src/MmsReportController.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ied_simulator/src/MmsReportController.hpp b/apps/ied_simulator/src/MmsReportController.hpp index 2c34482ba..4b5b593e6 100644 --- a/apps/ied_simulator/src/MmsReportController.hpp +++ b/apps/ied_simulator/src/MmsReportController.hpp @@ -150,7 +150,7 @@ class MmsReportController : public QObject { QVariantMap selectedRcb_; QStringList selectedDataSetMembers_; QString pendingStaticDataSetReference_; - QString pendingStaticContextFingerprint_; + QString pendingStaticAuthorityKey_; QVariantList receivedReports_; QStringList events_; QStringList diagnostics_; From 953ee2db9486f1c105858d11c65e80eb02f42802 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:48:23 +0700 Subject: [PATCH 18/27] refactor(report): name persisted static route authority precisely --- apps/ied_simulator/src/MmsReportController.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/ied_simulator/src/MmsReportController.cpp b/apps/ied_simulator/src/MmsReportController.cpp index 08e960250..bd8de1058 100644 --- a/apps/ied_simulator/src/MmsReportController.cpp +++ b/apps/ied_simulator/src/MmsReportController.cpp @@ -538,7 +538,7 @@ void MmsReportController::setHost(const QString& value) { const bool routeChanged = !pendingStaticDataSetReference_.isEmpty(); host_ = normalized; pendingStaticDataSetReference_.clear(); - pendingStaticContextFingerprint_.clear(); + pendingStaticAuthorityKey_.clear(); emit configurationChanged(); if (routeChanged) emit selectionChanged(); } @@ -548,7 +548,7 @@ void MmsReportController::setPort(const int value) { const bool routeChanged = !pendingStaticDataSetReference_.isEmpty(); port_ = value; pendingStaticDataSetReference_.clear(); - pendingStaticContextFingerprint_.clear(); + pendingStaticAuthorityKey_.clear(); emit configurationChanged(); if (routeChanged) emit selectionChanged(); } @@ -561,7 +561,7 @@ void MmsReportController::setEngineeringContext(IedEngineeringContextController* // DataSet selection across a different IED/context pointer. const bool routeChanged = !pendingStaticDataSetReference_.isEmpty(); pendingStaticDataSetReference_.clear(); - pendingStaticContextFingerprint_.clear(); + pendingStaticAuthorityKey_.clear(); if (routeChanged) emit selectionChanged(); if (engineeringContext_) { connect( @@ -661,12 +661,12 @@ void MmsReportController::adoptEngineeringInventory() { const auto seed = buildContextDiscoverySeed(canonical); auto ui = buildDiscoveryUi(seed, QString{}); if (!pendingStaticDataSetReference_.isEmpty() && - (pendingStaticContextFingerprint_.isEmpty() || - pendingStaticContextFingerprint_ != staticRouteAuthorityKey())) { + (pendingStaticAuthorityKey_.isEmpty() || + pendingStaticAuthorityKey_ != staticRouteAuthorityKey())) { appendDiagnostic(QStringLiteral( "Cleared stale static DataSet route after engineering context changed.")); pendingStaticDataSetReference_.clear(); - pendingStaticContextFingerprint_.clear(); + pendingStaticAuthorityKey_.clear(); } // An opened SCL model has no live RCB probe response yet. Preserve its // verified canonical DatSet binding for read-only Browser navigation. @@ -942,7 +942,7 @@ void MmsReportController::refreshSelection() { bool MmsReportController::selectRcb(const int row) { if (row < 0 || row >= reportControls_.size()) return false; pendingStaticDataSetReference_.clear(); - pendingStaticContextFingerprint_.clear(); + pendingStaticAuthorityKey_.clear(); selectedRcbIndex_ = row; refreshSelection(); return true; @@ -955,7 +955,7 @@ bool MmsReportController::selectDataSet(const int row) { if (!pendingStaticDataSetReference_.isEmpty() && !sameDataSetReference(pendingStaticDataSetReference_, reference)) { pendingStaticDataSetReference_.clear(); - pendingStaticContextFingerprint_.clear(); + pendingStaticAuthorityKey_.clear(); } selectedDataSetIndex_ = row; selectedDataSetMembers_ = dataSets_.at(row).toMap() @@ -1017,7 +1017,7 @@ bool MmsReportController::selectStaticRcbForDataSet(const int row) { return false; } pendingStaticDataSetReference_ = reference; - pendingStaticContextFingerprint_ = authorityKey; + pendingStaticAuthorityKey_ = authorityKey; emit selectionChanged(); return true; } From 82e5220cafb11ce3098767cec372c5e3a203ebdf Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:48:26 +0700 Subject: [PATCH 19/27] test(report): follow static route authority naming --- apps/ied_simulator/test_browser_report_authoring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ied_simulator/test_browser_report_authoring.py b/apps/ied_simulator/test_browser_report_authoring.py index 9358b31ad..3aae5bc8c 100644 --- a/apps/ied_simulator/test_browser_report_authoring.py +++ b/apps/ied_simulator/test_browser_report_authoring.py @@ -39,7 +39,7 @@ def require(text: str, token: str, label: str) -> None: "Static/non-owned DataSet binding is immutable", "bool MmsReportController::selectStaticRcbForDataSet(", "QString MmsReportController::staticRouteAuthorityKey() const", - "pendingStaticContextFingerprint_", + "pendingStaticAuthorityKey_", "Selected static DataSet has no verified live RCB binding", "sameDataSetReference(bound, reference)", 'dataSet.value(QStringLiteral("directoryAvailable")).toBool()', From b816f57c2c93bc231e2d3d5e1d3bf39cff7b7672 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:50:11 +0700 Subject: [PATCH 20/27] test(browser-ux): capture exact rendered Browser state from routing QA --- apps/ied_simulator/src/main.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/ied_simulator/src/main.cpp b/apps/ied_simulator/src/main.cpp index c9f029e23..d04c9ba58 100644 --- a/apps/ied_simulator/src/main.cpp +++ b/apps/ied_simulator/src/main.cpp @@ -433,7 +433,9 @@ int main(int argc, char* argv[]) { } } if (parser.isSet(qaBrowserRoutingOption)) { - QTimer::singleShot(150, &app, [&app, rootObject] { + const auto routingScreenshotPath = parser.isSet(screenshotOption) + ? parser.value(screenshotOption) : QString{}; + QTimer::singleShot(150, &app, [&app, rootObject, routingScreenshotPath] { auto fail = [&app](const char* reason, const int code) { qCritical().noquote() << "BROWSER_FLEET_ROUTING_FAIL" << reason; app.exit(code); @@ -763,6 +765,15 @@ int main(int argc, char* argv[]) { << "BROWSER_FLEET_ROUTING_PASS active_tab=QA_IED_B" << "offline_signals=visible" << "model_values=visible" << "value_label=bound" << "toolbar_1024=visible" << "switching=pass reindex=pass cross_ied_panel=false"; + if (!routingScreenshotPath.isEmpty()) { + auto* const window = qobject_cast(rootObject); + const auto image = window ? window->grabWindow() : QImage{}; + if (image.isNull() || !image.save(routingScreenshotPath)) { + fail("browser_screenshot_save", 78); + return; + } + qInfo().noquote() << "BROWSER_SCREENSHOT_PASS" << routingScreenshotPath; + } app.exit(0); }); } From 111032646108def5d2d56f8ed926c4a884eb5e9a Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:50:14 +0700 Subject: [PATCH 21/27] ci(browser-ux): upload rendered Browser screenshot from exact functional QA --- .github/workflows/ied-simulator-qt.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/ied-simulator-qt.yml b/.github/workflows/ied-simulator-qt.yml index 10d425ad1..cc3e6200e 100644 --- a/.github/workflows/ied-simulator-qt.yml +++ b/.github/workflows/ied-simulator-qt.yml @@ -360,6 +360,22 @@ jobs: if-no-files-found: error retention-days: 14 + - name: Capture IED Browser visual regression + env: + QT_QPA_PLATFORM: offscreen + run: >- + ./build-ied-simulator-qt/arstack_ied_simulator + --qa-browser-routing + --screenshot ./ied-browser-workbench.png + + - name: Upload IED Browser screenshot + uses: actions/upload-artifact@v4 + with: + name: ied-browser-workbench + path: ied-browser-workbench.png + if-no-files-found: error + retention-days: 14 + - name: Prove GUI value is visible through MMS run: >- python3 apps/ied_simulator/test_gui_live_value.py From 0ee8c21280ff158d5bfdea022fd59a9aa2063088 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:50:22 +0700 Subject: [PATCH 22/27] build(qt): include QImage for Browser screenshot QA --- apps/ied_simulator/src/main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/ied_simulator/src/main.cpp b/apps/ied_simulator/src/main.cpp index d04c9ba58..c62f8c9c5 100644 --- a/apps/ied_simulator/src/main.cpp +++ b/apps/ied_simulator/src/main.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include From ffbbb1f0507dcc707243f5970fd30994b4cb5ad9 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:50:48 +0700 Subject: [PATCH 23/27] ci: deduplicate push and PR validation for the same workbench branch --- .github/workflows/ied-simulator-qt.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ied-simulator-qt.yml b/.github/workflows/ied-simulator-qt.yml index cc3e6200e..02177a883 100644 --- a/.github/workflows/ied-simulator-qt.yml +++ b/.github/workflows/ied-simulator-qt.yml @@ -40,7 +40,7 @@ on: # Superseded commits must not occupy CI runners or mask the exact-head result. concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} cancel-in-progress: true permissions: From c4d34cf9654726c295c3b2659809b92c308c1e5e Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:50:52 +0700 Subject: [PATCH 24/27] ci: deduplicate push and PR validation for the same workbench branch --- .github/workflows/ied-simulator-release-hardening.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ied-simulator-release-hardening.yml b/.github/workflows/ied-simulator-release-hardening.yml index 2800850ce..e919a33fe 100644 --- a/.github/workflows/ied-simulator-release-hardening.yml +++ b/.github/workflows/ied-simulator-release-hardening.yml @@ -13,7 +13,7 @@ on: # Superseded commits must not occupy CI runners or mask the exact-head result. concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} cancel-in-progress: true permissions: From c73456c1777446be3b9555af3b0cadc01e72edfd Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:51:20 +0700 Subject: [PATCH 25/27] test(report): verify canonical DataSet row while allowing equivalent MMS reference syntax --- apps/ied_simulator/src/ied_report_workspace_qa.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/ied_simulator/src/ied_report_workspace_qa.cpp b/apps/ied_simulator/src/ied_report_workspace_qa.cpp index eef449dda..58ce8a5cd 100644 --- a/apps/ied_simulator/src/ied_report_workspace_qa.cpp +++ b/apps/ied_simulator/src/ied_report_workspace_qa.cpp @@ -367,8 +367,13 @@ int main(int argc, char* argv[]) { << "error=" << reports.lastError(); return 30; } - if (reports.selectedRcb().value(QStringLiteral("dataSet")).toString() != staticRoute) { - qCritical() << "REPORTS_WORKBENCH_FAIL static_route_dataset_changed"; + const auto restoredDataSets = reports.dataSets(); + const int restoredDataSetRow = reports.selectedDataSetIndex(); + if (restoredDataSetRow < 0 || restoredDataSetRow >= restoredDataSets.size() || + restoredDataSets.at(restoredDataSetRow).toMap() + .value(QStringLiteral("reference")).toString() != staticRoute) { + qCritical() << "REPORTS_WORKBENCH_FAIL static_route_dataset_changed" + << restoredDataSetRow << staticRoute; return 31; } qInfo() << "STATIC_REPORT_ROUTE_RESTORE_PASS" From a147618f2e7c6626815cb7a60ef1f3ab7193d47d Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:52:19 +0700 Subject: [PATCH 26/27] fix(report): restore best selectable static RCB from pool evaluation, not first matching row --- .../ied_simulator/src/MmsReportController.cpp | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/apps/ied_simulator/src/MmsReportController.cpp b/apps/ied_simulator/src/MmsReportController.cpp index bd8de1058..4959eb2e5 100644 --- a/apps/ied_simulator/src/MmsReportController.cpp +++ b/apps/ied_simulator/src/MmsReportController.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -990,18 +991,54 @@ bool MmsReportController::selectStaticRcbForReference( return false; } + QString preferredReference; + if (requireLiveProbe) { + int bestRank = std::numeric_limits::min(); + bool sawBoundEvaluation = false; + for (const auto& value : staticCandidates_) { + const auto candidate = value.toMap(); + if (!sameDataSetReference( + reference, candidate.value(QStringLiteral("dataSet")).toString())) { + continue; + } + sawBoundEvaluation = true; + if (!candidate.value(QStringLiteral("selectable")).toBool()) continue; + const int rank = + candidate.value(QStringLiteral("score")).toInt() + + (candidate.value(QStringLiteral("preferred")).toBool() ? 1'000'000 : 0); + if (preferredReference.isEmpty() || rank > bestRank) { + bestRank = rank; + preferredReference = candidate.value(QStringLiteral("reference")).toString(); + } + } + // The pool selector explicitly evaluated bound RCBs but found none safe. + // Do not bypass that decision with a raw first-row fallback. + if (sawBoundEvaluation && preferredReference.isEmpty()) return false; + } + for (int rcbRow = 0; rcbRow < reportControls_.size(); ++rcbRow) { const auto control = reportControls_.at(rcbRow).toMap(); const auto bound = control.value(QStringLiteral("dataSet")).toString(); + const auto controlReference = + control.value(QStringLiteral("reference")).toString(); if (bound.isEmpty() || control.value(QStringLiteral("dynamicBinding")).toBool() || (requireLiveProbe && !control.value(QStringLiteral("probeOk")).toBool()) || + (!preferredReference.isEmpty() && controlReference != preferredReference) || !sameDataSetReference(bound, reference)) { continue; } selectedDataSetIndex_ = dataSetRow; selectedRcbIndex_ = rcbRow; refreshSelection(); + appendDiagnostic(QStringLiteral( + "Static DataSet route selected · %1 → %2%3") + .arg( + reference, + controlReference, + requireLiveProbe + ? QStringLiteral(" · live verified") + : QStringLiteral(" · canonical"))); return true; } return false; From b47ac165f4ae9a3927cb81641cefc88df3e477dc Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Mon, 28 Sep 2026 05:52:34 +0700 Subject: [PATCH 27/27] test(report): preserve exact DataSet while allowing safer live RCB pool choice --- apps/ied_simulator/src/ied_report_workspace_qa.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ied_simulator/src/ied_report_workspace_qa.cpp b/apps/ied_simulator/src/ied_report_workspace_qa.cpp index 58ce8a5cd..866ef0ea8 100644 --- a/apps/ied_simulator/src/ied_report_workspace_qa.cpp +++ b/apps/ied_simulator/src/ied_report_workspace_qa.cpp @@ -357,7 +357,7 @@ int main(int argc, char* argv[]) { !reports.connectToIed() || !waitUntil([&reports] { return reports.connected() && !reports.busy(); }, 12'000) || reports.staticRouteDataSet() != staticRoute || - reports.selectedRcb().value(QStringLiteral("reference")).toString() != originalRcb || + reports.selectedRcb().value(QStringLiteral("reference")).toString().isEmpty() || !reports.selectedRcb().value(QStringLiteral("probeOk")).toBool() || reports.selectedDataSetMembers().isEmpty()) { qCritical().noquote() << "REPORTS_WORKBENCH_FAIL static_route_restore"