diff --git a/.github/workflows/apply-scl-workflow-layout-repair.yml b/.github/workflows/apply-scl-workflow-layout-repair.yml new file mode 100644 index 00000000..639491bc --- /dev/null +++ b/.github/workflows/apply-scl-workflow-layout-repair.yml @@ -0,0 +1,110 @@ +name: Apply repaired SCL workflow and layout + +on: + pull_request: + branches: [ main ] + +permissions: + contents: write + +jobs: + apply: + if: ${{ github.event.pull_request.head.ref == 'fix/scl-workflow-layout-repair' }} + runs-on: ubuntu-latest + steps: + - name: Checkout repair branch + uses: actions/checkout@v4 + with: + ref: fix/scl-workflow-layout-repair + fetch-depth: 0 + + - name: Apply and validate reviewed transformations + id: transform + continue-on-error: true + shell: bash + run: | + set -u + rm -f .repair/apply-error.txt + set +e + { + set -x + python3 -m py_compile .repair/apply.py + python3 .repair/apply.py + base64 --decode .repair/app-icon.b64 > Assets/app-icon.ico + grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml + ! grep -q 'Content="Confirm Open"' MainWindow.xaml + ! grep -q 'Content="Confirm Close"' MainWindow.xaml + grep -q 'TryClaimControlConfirmation' MainWindow.xaml.cs + grep -q 'SclFindingAggregator.Group(actionableFindings)' MainWindow.xaml.cs + grep -q '1.6.9' ArIED61850Tester.csproj + test -s Assets/app-icon.ico + } > .repair/apply-error.txt 2>&1 + status=$? + set -e + exit "$status" + + - name: Upload transformation failure evidence + if: steps.transform.outcome == 'failure' + uses: actions/upload-artifact@v4 + with: + name: scl-workflow-repair-error + path: .repair/apply-error.txt + if-no-files-found: error + + - name: Stop after transformation failure + if: steps.transform.outcome == 'failure' + shell: bash + run: exit 1 + + - name: Publish repaired non-workflow files + if: steps.transform.outcome == 'success' + env: + GH_TOKEN: ${{ github.token }} + TARGET_BRANCH: fix/scl-workflow-layout-final + shell: bash + run: | + set -euo pipefail + + update_existing() { + local path="$1" + local current_sha local_sha content payload + current_sha=$(gh api "repos/$GITHUB_REPOSITORY/contents/$path?ref=$TARGET_BRANCH" --jq .sha) + local_sha=$(git hash-object "$path") + if [ "$current_sha" = "$local_sha" ]; then + echo "$path already current" + return + fi + content=$(base64 -w0 "$path") + payload=$(jq -n \ + --arg message "fix: publish repaired $path" \ + --arg content "$content" \ + --arg sha "$current_sha" \ + --arg branch "$TARGET_BRANCH" \ + '{message:$message,content:$content,sha:$sha,branch:$branch}') + gh api --method PUT "repos/$GITHUB_REPOSITORY/contents/$path" --input - <<<"$payload" >/dev/null + } + + create_or_update() { + local path="$1" + local current_sha local_sha content payload + local_sha=$(git hash-object "$path") + current_sha=$(gh api "repos/$GITHUB_REPOSITORY/contents/$path?ref=$TARGET_BRANCH" --jq .sha 2>/dev/null || true) + if [ -n "$current_sha" ] && [ "$current_sha" = "$local_sha" ]; then + echo "$path already current" + return + fi + content=$(base64 -w0 "$path") + if [ -n "$current_sha" ]; then + payload=$(jq -n --arg message "fix: publish repaired $path" --arg content "$content" --arg sha "$current_sha" --arg branch "$TARGET_BRANCH" '{message:$message,content:$content,sha:$sha,branch:$branch}') + else + payload=$(jq -n --arg message "docs: add Smart Dynamic SCL workflow" --arg content "$content" --arg branch "$TARGET_BRANCH" '{message:$message,content:$content,branch:$branch}') + fi + gh api --method PUT "repos/$GITHUB_REPOSITORY/contents/$path" --input - <<<"$payload" >/dev/null + } + + update_existing MainWindow.xaml + update_existing MainWindow.xaml.cs + update_existing ArIED61850Tester.csproj + update_existing scripts/publish-windows-portable.ps1 + update_existing Assets/app-icon.ico + create_or_update docs/SCL_SMART_DYNAMIC_REPORTING.md diff --git a/.repair/app-icon.b64 b/.repair/app-icon.b64 new file mode 100644 index 00000000..fb5d268c --- /dev/null +++ b/.repair/app-icon.b64 @@ -0,0 +1 @@ +AAABAAMAMDAAAAEAIACoJQAANgAAACAgAAABACAAqBAAAN4lAAAQEAAAAQAgAGgEAACGNgAAKAAAADAAAABgAAAAAQAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD///8A////AP///wD///8A////AP///wD///8A////AIgAAA+OBgAtjQMAXIwEAn6MBACJigQCiYkCAoqHAgKKgwICioECAoqAAAaKgAAHin4AC4p+AA+KegARinoAFop6AByKeAAfingCJYp4ACqKegAuinwANIp8AjeKfgA7in8APYl/Aj+JgABAfIEARluCAEkxeQBDEwAAAAH///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wCXAAAWkwcAbZMJAMZ+BwH2bQQB/WQDAf1gAwH9XgIC/VwCAv5bAQL+WQED/VgBA/5XAAT+VwEF/VQBB/5TAAj+UgEL/lIBDf1RARD+UgES/VIBFv1SARn9VAEc/VUBHv1XASH9WAEk/VkBJv1dASn9XgEs/WUBNPx0AUL4hQFP0YYCUYGHAFAgAAAAAf///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AqgAAA6UMAEGeDADMcwcB+1ECAf1HAAH9RAAB/UEAAf0/AAH9PQAB/T0AAf48AAH+OwEA/TsAAP47AAD+OgEA/jkBAP44AAH+NwAC/jcABP43AQb+NwEI/TgBCv05AQz9OgEO/jwBD/0+ARL9PgEU/UEBFf1DARf9RQAY/UcAGfxJARz9TwEh/WcBOPyGAVXeiwBZYYAAQAj///8A////AP///wD///8A////AP///wD///8A////AP///wC/AAAEtRIAYZYPAO5aBAH9RwAB/UIAAf0+AAH9OwAB/TkAAf04AAH9NwAB/TcBAf42AQH+NgEA/jcBAP43AAD+NgEA/jYAAf41AAL+NAAD/jQABf40AQj+NQAJ/TUBC/03AA39OAEP/ToAEf08ARP9PQEV/T4BFv1AARn+QgAZ/UUAGv1HARz9SgEd/U0BIP1VASf9egFN9o4CXoqAAE0K////AP///wD///8A////AP///wD///8A////AP///wDFGwBUnRQA8VEDAP1EAAH9PgAB/TsAAf05AAH9NwAB/TUAAP00AAD9MwAB/TIBAP4yAQH9MwEB/jMBAP4zAAD+MwEA/jMAAf4zAQP+MgAF/jIAB/4zAQn+NAAL/TUBDf43ABD9OAAS/ToAFP08ARf9PQEZ/T4BGv1BARz9QgAc/UQAHf1HAR79SQEf/UwBIf1QASP9VQEn/XoBTvaTAmR9gABABP///wD///8A////AP///wD///8A////ANImACi1HgDkVgQA/UQBAP0+AAH9OwAB/DcBAf01AAH9NAAB/TIAAP0xAAD9MQAB/TAAAP4xAAD+MAAA/TEAAP4xAAD9MQEB/jIAAv4xAQT+MQAG/jEACP4zAQr+NAAN/TUBEP04ABT9OQAW/TsBGf09ARz9PgEe/UEBH/1CASH+QwEi/UQBIv1HASP9SQEj/UwBJP1OASX9UQEl/VoBLP2IAVvwmABjQ////wD///8A////AP///wD///8A/wAAA9ErAKdnCgH5SAEB/T8AAf07AAH9NwAB/TUAAf00AAH9MgAA/TEAAf0wAAH9MAAB/TAAAP4wAAD9MAAA/S8AAP0wAAD9MQEC/TEABP4xAQb+MQAH/TEACf0zAAz+NQAP/TgBFf07ARn9PQEc/T4BIP1BASP9QwEm/UUBKP1GASr9RgEp/UgBKv1JASv9SgEq/UsBKf1NASj9TwEm/VQBKP1gATL8lgFmz4kAYg3///8A////AP///wD///8A4TcAKrEpAOxUBAH9QwEB/TwBAf04AAH9NgAB/DMAAf0zAAH9MwAA/TIAAP0yAAD9MQAA/TIAAf0yAAD9MgAA/TIAAf0xAAL9MQEE/TEABv0xAAf+MAEJ/TEAC/00AA/+NwAV/TwBHP1CASP9RAEo/UYALf1IATP9SwE3/UsBOv1OAT39TgE8/U8BPf1QAT79TwE5/U0BM/1NAS79TgEr/VIBKf1ZASz9fwFR8JsAa0X///8A////AP///wD///8A40IAbG0SAPRJAgH9PgAB/ToAAf03AAH9NgAB/DUAAf1CBAD9SQYA/UkFAP1JBAD9SAMA/UcDAf5IAgH9RgIE/UUCB/1EAgv9PgEO/TMACf0xAAn+MQAK/TIADf42ARP+OwAd/UwBNv1WAUr9WgFV/V0AYP1gAWr9YgFv/WMBc/1mAXr9ZgF7/GgBfvxoAXv9YAFl/VQBSf1PATn9TgEw/VABK/1UASr9YAEy+pgAaKT///8A////AP///wD///8A00IAuVoJAPtEAgH9OwAB/TgAAf02AAH9OAAB/EYFAP3DRQr93UwS/dpFE/3WPhT9zzcX/cwwHf3ILCb9xCc0/cAiRP27HVL9rRVf/UcBGv0yAAv+MQEL/TMBD/05ARj+SgE0/aMQqv24Gsr9uRvW/bsb4P29G+f9vR3r/b8e7vzEHu/9wyDx/MQi8fzIJPD8ogzL/WQBcv1VAUv9TwE4/VABMP1SASz9XAEx/pYBZuCAAE0K////AP///wC2SQAHzkUA4lcIAf1CAQH9OwAB/TgAAf03AAH9OwAB/IwnBf3zljP9+4sX/fuBFf36ehn+/HMl/ftqNf77ZEP9+15S/ftWZf38T3r990+d/YwWWv01AA7+MQEL/jQBD/08AB3+fAd1/OtK7v32MvX99zD4/fcx+f33Mvr99zX7/fY5+v34P/v990j6/fhU+v33avr921/2/YUCrf1eAWH8UwFE/FEBNf1SAS79WQEx/pMBZeibAG0c////AP///wDVVQASyUkB5lUJAf1EAgH9PQEB/ToBAf06AQH9Vw0A/ORyI/36hwj9+3cA/ftuAP37aAP9+2IM/fxbG/37VSf9+080/ftKRP37RFj9+zt4/dxEl/1TAiT9MwAM/TUAD/1NAS79xznC/fge6/33E+v99xHx/fcQ9f32EPf99xH5/fYT+v34GPr99xz5/fck+v34N/r99G76/b0p6P1xAYr8WgFV/VMBPf1QATP+WAEz/ooBXeqhAHIm////AP///wDnYQAVx0wB51gKAf1GAgH9QgEB/UEBAf1IAwD9sEUM/PacK/38gAL9+3MA/ftuAP37agL9+2UL/fxgGf37WyP9/FYu/fpQOv76S0j9+0Re/flCi/6gJWf9OAAO/TgAEv2JEW398zrd/fgW3v33E+P99xHr/fYQ8/32EPf99hH4/fcS+fz3FPr99hb5/fga+vz3JPr9+ET6/eh+9/2dCcn8ZwFz/FcBS/1TATn9WAE2/ooBXuumAIAo////AP///wDoaAAWx08B51sLAf1LAwD9RwIB/UsEAP19IQD97o8v/fuNBP38fAD9+3MA/fxwAP37bwL9+24L/ftpGP78ZiL9+2Er/fpbNP77Vj79+01O/ftCav7pS5L9YQQq/VYBJ/3YQbP9+R3L/fkY1P34Ft799xbp/fcX8/33GPf99xj6/fcZ+v34GPv99xf5/fcX+/z3HPr9+C36/fZZ+/3TT/P9hAGo/WEBX/1WAUL9WgE6/osBY+usAIYo////AP///wDoaAAWyVQB514NAf1OBQD9TAUB/V8OAP3Ubhn9+agZ/PuOAf38fgD9/HUA/fx0AP37fAb+/H8Q/ft+Hf78fCf9+3gw/vtyNP37Zjj9+1pF/ftOWP37RHf+tjNx/aIla/34Nrj9+iG6/fkcy/34G9v9+B/r/fgt9v35NPn9+TX6/fk0+v35MPv9+Cb5/fca+v33Gvn9+CP6/fk8+v3zffn9tiLg/W0Bd/1aAUn9WwE9/o0BZuusAIYo////AP///wDodAAWylgB52EPAf1QBwH9UAcB/WkSAP3LezT9+KcG/fuVAP37gwD++3gA/ft2AP7yjz/+uk4o/bVMKP23TSr9wU8s/euOVf77fDX9+mk8/vpYS/77S2L+8lSM/upPnv35L5/++iau/fkhxf34INn9+DTt/dlR7P3FNd/9wTbc/cE13/3LN+z971v5/fce+v33Gvr9+CD7/fk0+/31XPv9xEfp/XIBfv1bAUv9WwE+/o4BaOusAIwo////AP///wDodAAWy1sB52USAf1SCAH9UQYB/V8OAf2ZNQ/98bU6/PufA/37igD++30A/ft2AP73iBz+niMH/VsHAf5gCQD+cRAA/a1MKP74mUP9+300/ftnQf36VlL++khn/vtBfv36No3++i2i/fkmvf35Kdj95Fzn/ZYEo/17AYL9dQF9/XoDjvy6JNn99jb5/fYZ+v32GPr9+CH7/fg++/3uiff9ow3O/WwBb/1aAUf9WwE9/o8BauusAIwo////AP///wDodAAWy1wB52cUAP1VCQH9UgcB/VgLAf13GAD9x3U2/PioB/36lAD++4QA/fx7AP37egH+5HYu/lQJAv5JAQH9VQQB/XkWAf3ajlD9+5Mw/fp3N/76ZUT++lVT/vtKaf36P37++jWX/fovtv33Ttj9rCWt/XMBaf1jAVn9YgFc/YwGl/zoR/H99hr5/fcW+f33Gfr9+Sf7/fZS+f3IPOv9hAGY/WQBWv1YAUH9WgE7/o8BauusAIwo////AP///wDogAAWzF8B52sWAP1YCwH9UwkB/VUKAf1kEQH9lS8I/O+2R/36ogT++44A/fyBAP37eQD++ooa/qIsDf5DAgH9SAEB/V8KAP2cOxj99q1O/fqMMP76dTn++2NH/vxVWv38SXH9+z2L/fo8r/3OVLT9ewFl/WEBS/1aAUj9ZQNc/cg90Pz3Ifj99hX4/fcW+v33Hfr9+Db7/eN09f2bBbn9cQFv/V4BTf1XATv9WgE4/o8Ba+uzAJMo////AP///wDogAAWzmEB53AZAP1cDQH9VQoB/VUKAf1cDQH9dhkA/MNrLv34rQn9+poB/fyKAP38gAD9/HsA/el8Lv5XCgL+QgAB/U8EAP5zEwD90IRJ/fqgK/37hzH++3E9/vtgT/38U2X9/EaB/fFnsf6MDlv9ZQFB/VkBPP1bAkb9lwqR/fE68f33Fff99xX4/fcY+v34KPr99GL5/bgl2/1/AYX9ZgFY/FsBQ/1VATb+WgE3/pABa+uzAJMo////AP///wDogAAW0GQB53UbAP1fDwH9VwwB/VUKAP1YDAH9ZhIB/ZIqBf3ssk79+qYE/fuUAP38hwD9/H8A/fqJFP6rPBn9QgIC/UcCAP5fCQD9lS0K/fW3U/36mir++4I0/vtuRf37X1n+/FN5/dhXj/2zFmf9rRVx/a0Uf/2zFZj930XZ/fcX8f33FPf99hX4/fcd+f34OPv93F7z/ZMBpf1vAWb9YAFM/VgBO/1VATT9WwE3/pEBa+uzAJMo////AP///wDoiwAW0WgB53oeAf1kEgH9Ww4B/VYMAf1XDAD9Xg8B/XYYAf29Yij997IP/fugAf37kQD+/IYA/fuBAP7sgyr+YQ0C/kEBAf5SBAD9dRUA/cl6P/36sC3++5Yu/vqAO/77bk7+/GFm/ftigv76VI39+Ueg/fg5uP33MdD9+CDi/fcT7f33FPb99hj4/fgp+v3zdvj9shPT/XwBe/1lAlP9WwFC/VcBNv1UATH+XAE2/pIBb+uzAJko////AP///wDoiwAW0mwB534gAf5pFAH9XxAA/VkNAP1YDAD9Wg0B/WcRAP2NJgH95ahL/fquBP76nQD+/I8A/fuGAP76jAz+uU0i/UADAv5IAgH9XwoB/ZMoBP3vuVb++60q/vmUNP76gEP++3BV/fpfaP77TXz9+zuQ/fkqq/34HsX9+BfZ/fcU6v33FvX9+B/4/fc++v3TRu79kAGc/W8BYf1fAkj9WQE6/VYBM/1UATD+XQE2/pkBd+u5AKYo////AP///wDolwAW03EB6IUkAf5uFwH9YxIA/VwPAP1ZDQH9Wg0B/GEQAP12GQD9tVcg/fa9Hv77qgH++5gA/vuOAP77iAD+8Ysm/moRA/5CAAH9UgQB/XUVAP7BajP++sQ3/vqoMv76kz/++39Q/fpoZf77U3v9+z6P/forp/34H8H9+BjY/fcW6v34GfX++C35/e17+P2qB8X9fAF2/WYBUf1dAj/9WAE2/VQBMf5VATD+XwE4/pcBdeuzAJ8o////AP///wDolwAW1XUB6IgmAf5zGQH9aBQA/WARAf1bDgH+Ww4A/V4PAP1rFAD+jSUA/d+dRP36tgT++6QA/vuWAP77jQD++4wG/sRcKv5BAgL+SQEB/V8LAf6PJAH+6rRa/fu+NP76qED++pFS/vx3aP37XX/9+0WS/fkup/34IsH9+BvX/fcY6v34I/b+9k75/csy5v2MAZD9cAFe/WIBR/1bAjv9WAE0/VcBMf1XATH+YQE6/pkBd+u5AKYo////AP///wDolwAW1noB6I8rAf55HAH9bRYA/WQTAP1eEAD+XA4A/F4PAP1lEgD9eBoA/bBMFf32xzT++7EC/vugAP77lQD++4wA/vSTJv53FAX9QQEB/lADAf1sEAD+r1Ei/fjQWP36u1X9+aRj/fmLeP34b4v9+lGd/foyq/34JMP9+B3a/fge7f35M/n95mr0/aACr/14Amv9aAFO/V8BQf1aAjf9WAEz/VgBMf1aATL+ZAE8/psBfOu5AKYo////AP///wDoogwW2X8B55QvAP1+HwD9chkA/WoWAP1jEwD9XxEA/V8PAP1jEQD9bRUA/Y4mAP3bkjz9+74F/fuqAP77nAD++pMA/vuQA/7Taiv9RAMC/UYBAP1YBgH+dhMA/p4vD/2nMBf9oyYf/aAfLv3HN2H9+luo/fkyrv34J8b9+CHe/fgp8P31afj9wB3X/YQCfv1tAlb9ZAFF/V8BO/1bAjX9WgIz/VwBM/1fATX9aAE//p4Bf+u/AKwo////AP///wDoogwW24UB6JszAP6FIwD9eB0A/XAZAP1pFQD9ZBIB/WERAf1jEAD9aRQA/XkbAP2sRQv99sxE/fu5Av77pwD++pwA/vuUAP73niL+hhsJ/j8AAf1HAQH+VAMB/mEFAP1mBAH+agMG/YgIGP3pYIr9+kOZ/fkysP34Kcr9+Sjj/fg99f3gUfD9mwGg/XUBYv1nAkv9YgE+/V4BOP1cAjX9XQE1/WABNv1iAjn+awJC/qEBhOu/ALMo////AP///wDorgAW3YoB6KE5AP6LKAD9fyAA/XYbAP1tFwD9ZxUB/GQSAf1kEQD9ZhMA/W8WAP2LJQD91Ic0/frGCf77swD++6UA/vqbAP77lAH93HUu/kcEAv49AAH9RAEB/UkBAf5OAQH+WgIE/cBCSf36WIz9+kGY/fk1sv34Ls/9+TTp/fJ79v25C8j9hAJ3/WwCUf1kAkP9YQI7/V8BOP1fATb9YAE3/WQBOf1nAj3+cAJH/qMBieu/ALko////AP///wDorgAW4JEB56g/AP2SLQD9hiUA/XwgAP10GgD9bBcB/WkUAf1lEwD9ZhMA/WsVAP18HAD9qT4F/fLHSv77wQL++68A/vqkAP77mwD9+aAc/pIoE/44AAH9OgEB/T4AAf5EAQH+gQ4M/fJxe/37UIP9+UOa/vk4uP35Ndf9+E7w/dRB4v2YAZH+dgFe/WgBR/1jAj79YgI6/WECOP1iATn9ZQE7/WkBPv1sAkH+dQJM/qcBjuvGAL8o////AP///wDouQAW4pcB565GAP6bMwD9jisA/YQkAP17HgD9cxoA/W4XAP1qFQD9aBQA/WsVAP1zGQD+jCYA/c96L/76zhD++7wA/vutAP76owD++5wA/uKBMf5OBQH+NQAB/jgBAf5KAwH+01xL/ftibP76UIP++Uae/vo8wf35QuH97XXx/a0Erf2CAWv+bwFO/WYCQf1jATv9YwE5/WQBOv1mATv9agE+/W0CQv5yAkf+fAJS/qoBlevGAMYo////AP///wDouQwW454B57RNAP2jOQH9ljAA/YspAP2BIQD9eR0A/XQbAP1vGAD9bBYA/WwVAP1xFwD9fh4A/ac6Av7tvUj+/MoC/vu4AP77rAD9+qQA/vuhE/6jOx/9MwAA/jUAAf6JGg/++IVm/fxiav37VIj9+kqn/flEy/34Z+v9zyzR/ZABfP12AVX9agFE/WUCPf1kATr9ZgE7/WgBPP1rAT/9bwJE/XQCSf16AlD9gwJa/q4BmuvGAMwo////AP///wDoxQwW5qQB57tUAP2qQQD9njYA/ZMuAP2JJgD9fyEA/XseAP11GwD9cRgA/XAXAP1xFwD9eBoA/Y4mAP3Ibib++tch/vvDAf77tgD9/KwA/fulAP7qki/+WwcC/kwDAP3bcEv9/HZQ/ftnbv37WZD9+lC0/flQ2v3nZOj9qAKY/X8BX/1wAUn9aQE//WYCO/1nAjv9agE9/W0BQv1xAUb9dQJK/XsCUP2BAlj9igJi/rEBoevRANEn////AP///wDoxQwW560B6MJbAP2xRwD9pj0A/Zs0AP2SLQD9iCYA/YIhAP17HgD9dxsA/XQZAP1zGAD9dxkA/YIfAP2lOAH967ZI/fvQAv77wgD9+7YA/fuvAP77qwr+tlAn/Z4vG/36kk79+3pQ/vtudf37YZv9+lrD/fZ+6P3EF7f9jwFu/XYBTv1uAUP9aQE9/WkCO/1rAj79bwFD/XMBR/14AUz9fAJR/YICWf2IAl7+jwJq/rYBp+vRB9gn////AP///wDo0QwW6bYB6MhiAP65UAH9rkUA/aQ8AP2aNAD9kSwA/YknAP2DIgD9fh8A/XocAP13GwD+eBoA/X0eAP6RKQD9x2Ub/frcMP38zgH9/MIA/fu4AP77sgD+8qQr/uuPSP37kDv+/INX/ft2fv37aqb9+m3R/eJa1/2jAoP9gQFY/XMBR/1uAUL9awI9/W0BP/1wAkP9dQFI/XkBTf19AlP9ggJZ/YkCYP2PAmj+lwJ0/rsBr+vRB94n////AP///wDo3AwW670B6M5qAP7CWAD9tk4A/a1FAP2kOwD9mjMA/ZMtAP2MKAD+hiMA/YEgAP1+HgD9fBwA/X0eAP6IIwD+pzgA/uitQ/372gb9/M4C/fvDBP77uwb++7IS/vuoLv37mUL+/Ixi/ft+i/37dbf99JHi/cANn/2PAWP9ewFN/XMBRP1vAkD+cAJB/XIBRP12Akj9ewFP/X8BVP2EAlr9iQJh/ZACav2WAnP9nQGA/r8BtuvRB94n////AP///wDn2wwV7ckB59Z1AP3KYgD9wFgA/bdOAP2tRAD9ozoA/Z0zAP2VLQD9jikA/YgkAP2EIQD9gR8A/YAeAP6EIQD+lSsA/cZeD/3530P9/NsI/fzMCf77ww7++7kb/vqtMf77ok3++5Vu/fuKmv36kM7930nD/aECc/2EAlP9eQFI/XMCQ/1yAkL+dQJF/XgBSf18Ak/9ggJW/YYCXP2LAmT9kAFr/ZcBdf2dAn/9pAKM/cUDwOrXB90m////AP///wDU1AAM8dUB5dx/Af3TbAD9ymEA/cFXAP23TQD9rUMA/KY7AP2eNAD9li8A/ZAqAP2KJQD9hyMA/YUhAP2GIQD+jiUA/as7AP3koT79++YX/vzXEv77zRr++8Mo/vq4Pv77rFv++5+A/fqXr/3xkNb9uguI/ZABWv2AAkv9eQFG/XYBRf14AUf9egJK/X4BT/2CAVf9iAFe/Y0CZv2SAm/9mAJ2/Z8Cgf2lAo39rQOa/tEI0enWCN4f////AP///wAAAAAB7NYCzuGLAPzZdwD902wA/cthAP3BVgD9tUwA/a9EAP2nPAD+nzUA/ZgvAP2SKgD9jicA/YskAP2KIwD+jSQA/Z0uAP3GWQj9+N9U/fvkHv772Sf+/NA2/vvFTf77uG7++quX/fmz[... ELLIPSIZATION ...]//8A7t4ChOiZAPbfggD92ncA/dNtAP3KYQD9v1UA/LhNAP2wRQD9qDwA/aA1AP2ZLwD9lSsA/ZIoAP6PJQD+kCUA/ZkqAP2uOgL945M8/fzwQv375jj++95I/vvTY/77xob++720/e+Gyv24DXT9lQJU/YgBSv2DAUr9ggFM/YIBT/2FAlP9iAJY/YwCX/2QAmj9lgFx/ZwCev2iAoX9qAKP/a8CnP21Aqj9vwK3+twN47X///8A////AP///wD///8A7OwFNvC+AO/mkAD934IA/dp3AP3TbAD9yV8A/MJWAP25TQD9sEUA/ak8AP2iNAD9nC8A/ZcrAP6VKAD9lCcA/ZcoAv6iLQX9wkoO/fPMbf3763X+++WA/vvek/781Kz++cXO/dIoiP6kB1n9kgFM/okBS/2HAk39iAFQ/YgCVf2MAlv9jgJh/ZMCaf2XAnL9ngJ9/aQChv2qApL9rwKc/bYCqf2+Arb9zAfM8uIS6Ff///8A////AP///wD///8A29sAB/HeAMLtowD75o4A/eGCAP3bdwD90moA/MtgAP3DVgD9uU0A/bFEAP2pOwD9ozQA/Z4vAP6aKwD+mSkB/pknBP6eKAn9qy0Q/shJJP7cXTn+31xE/t9WTv7gT17+1TZn/bAQVf6aBk39kQJM/Y0BTf2NAlL9jgJX/Y8CXf2TAmT+lgJq/ZsCdP2fAn79pgGJ/asCk/2yAqD9twKr/cACuP3IAsj84xLo3tML3hf///8A////AP///wD///8A////AO3oADj0zADt7J8B/eePAP3iggD92XQA/dNqAP3MXwD9wVUA/bhLAP2xQgD9qjoA/aU0AP6hLwD+nisC/Z0oBv6dJgr9oyUR/aolGf6wJSH+syQo/rIfL/6wGjj+qhNB/Z8LR/2YBUn+kwJO/ZICU/2TAlj9lAFf/ZYCZv2aAm7+nQJ1/aICf/2nAov9rgKV/bQCof26Aq/9wQK6/ckCyv3ZDODz5BbsXv///wD///8A////AP///wD///8A////AP//AALv4wBu9MQB9e6fAP3ojgD94IAA/dp0AP3TaQD9yl0A/cFSAP25SgD9skIA/a06AP6oNAD+pC8C/aIrB/6iJwz9oyMS/aUgGv6mHSD+phsm/qQXLv2jEzX9oQ4+/ZwJRv2aBE39mAJT/ZgBWv2aAmL9nAJp/Z4Ccv2jAnz9pQKC/aoCjf2wApr9tgKl/bwCsf3DAsD9ygLO/dYJ3/nnGO+dxgDjCf///wD///8A////AP///wD///8A////AP///wDf3wAI8OQAevTIAPXuoQD95o0A/eGAAP3bdQD90mgA/cpcAP3DUwD9u0sA/bZCAP6xOwD+rDQD/qgvCf6nKg79pyUU/qcgHP6mHCP+pRkq/qMVMv6jETr9og1C/aAISv6eBFL9ngJb/Z8CZf2iAm79pQJ3/acCgf2tAoz9rwGT/bQCn/25Aqv9wAG3/cUBxP3LAtP92w/k+Ocf76LjHOMS////AP///wD///8A////AP///wD///8A////AP///wD///8A//8ABe3kAFT02ADW7q8A/OiRAP3iggD92nQA/dNpAP3MXgD9xVQA/b5KAP65QgD+tDsE/bE0Cv2uLg/9rSkW/a0jH/6sHif+qhsv/qgWN/2pEj/+qA5J/aYKU/2mBlz9pwJn/akCcv2sAn79rwKJ/bIClf24AqH9ugGo/cABtP3FAcL9zAHP/dUJ3vzhG+vm5SPtdNgU6w3///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wDs4wAb8OAAdfLPAMXusQDy6JoA/eOLAP3efgD92HQA/dRqAP3QYgD9zVkD/cpSCP3ISw39xkMW/cY6Iv3FNSz9wi81/cEpP/3CJUr9wSBW/cEbYv3BF279whJ9/cMPjf3GDZz9yAys/coLufzNC8T90AvM/NcO2fzdFOL15R/qzugm7obqIuolqgCqA////wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAAAe3bAA7x1QAk78YAP+y3AFLtqwBV6qIAVeqWAFXqkgBU54kAVOeDA1TneQZU5HAMVORnGFTkYSdU4Vg0VOFSQFTkTExU5EZbVOFAalThOnlU4TGSVOQup1TkJLlU5CHGVeQe1VXkHt5V4x/jUuQi6EPsJuwo4R7hEYAAgAL///8A////AP///wD///8A////AP///wD//wAD8OAAIfDEAFbrrwCC5psAl+KLAJjdfACY2HACmNVlB5jTWQ+Y0ksimNBBMpjOO0OY0DJUmM4qaJjOIoGY0hucmNUWtZjXFMiY2BTTl94X4IXhHOZc5SHrJ8wz/wX///8A////AP///wD///8A8AAAD+AAAAfAAAADgAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAHAAAAD4AAAA/AAAA8oAAAAEAAAACAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAYAAAAx4BgBVZQMAtlwBANpYAQHeVQAC3lIAB95QAA7dUgAY3VUAIN1ZACfaYQAxuG8APVx2AE4NgAAAAoAAAAiPEgBwTQIA7ToAAfo2AAD8NAAA/DUAAPwzAAP9NQAJ/DkAEfw9ARj8QgEd/EYBIPpVASnweABKf3EAOQmoJAAjZgwA8TwBAfxHCQL9TgwE/U0JCP1JBhD9NAEJ/TcBE/1TBTv9XAZP/WAHWPxhB1b9UQI2/WIBN/WCAFstpDEAVEcDAfpEBgH8x10S/d9cEf3dSy392Dpb/WEOMP1eCkP91SDR/dsc4v3eJOj82D/k/XULfvxWAjf6gQBWX6M5A15RCAD6mUQO/Pd/Bf32bQr98mIl/fdWQ/3INWT9wiSS/fUZ3P3vHfD98h73/PQj+P3DOdL8YgRQ+4MAX2moQQNeWwwA+qVRFfz4iwL95GwQ/YQmDv3abDP99VlT/fQ4i/3mK8P9lxSX/cgh1fz0Hvj9zzva/WYFVfuGAGRprEgAX2EQAPtsGAP84owX/fWBBf2OMQ39kDsZ/fSBOP35VWf9wzeI/YwQdv3rIOv88DD1/ZQXmv1bAkD7hwBlaLRTAF9sFQH7XA4A/JxGC/3ylAj9124K/mYVBv3LdCr9+X9K/fNQf/3xJr799R/u/cQn0P1oAlX9WAE2+4kAZ2i8XgNfeh0A+2ERAP1zHwT92YoQ/fWSBP6KNAn9fywO/dR8P/3rWXz99ynC/eYs4/2IDH/9XQE8/VwBNvyRAm5ox24DX4wqAPtuFwD8aRUB/aJMD/32rwr+4YIN/VgOBf1nDw394kl5/fY1zP26KLb9bANO/WABOP1pAj/7mAJ9aM+BA1+iOwD7gSIA/XEYAP17HwH91YwX/viqA/2lUxL9njwr/fdXif3kRsv9hAVp/WcBP/1sAUH9egJP+6QCjmjZlgNfuE8A+5kzAP2DIgD9fB4B/aFGCf3yuQr976UP/u+FRP32ap79rB2K/XMDSP1xAUT9fQFS/Y0BafuxAqJo5LIAVtBrAPq1SwD8nDQA/YsmAP2OKQH92Zoc/fnIIP76qWD932ek/YsIV/15AUn9ggFW/ZIBbf2kAYr7xQW9YOvEACfllwDzz2kA/LdMAP2hNQD9lioB/a9EDv3rqU797ZmB/bIhbP2KAlD9iwFb/ZcBcv2pApD9wAO29tUK1THfnwAI7L4AeuOPAO/QaAD6uUkA+6g2AfylKg37rCQj/KsZO/udB0/7mgJj+6MCe/uvAZj6wgS78dYL2orGAMYJ//8AAf+/AAzuuQBY5ZoAsNZ2AMnKWgHKwUUOyr0yKMq7I0TKuhVmyr8NkMrGCrTJzw3Ns9kQ3l7dEd0PgACAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= \ No newline at end of file diff --git a/.repair/apply.py b/.repair/apply.py new file mode 100644 index 00000000..f56ee4e3 --- /dev/null +++ b/.repair/apply.py @@ -0,0 +1,128 @@ +from pathlib import Path +import re + + +def read(path: str) -> str: + return Path(path).read_text(encoding="utf-8") + + +def write(path: str, text: str) -> None: + Path(path).write_text(text, encoding="utf-8", newline="\n") + + +def sub_once(text: str, pattern: str, replacement: str, label: str, flags: int = 0) -> str: + if replacement in text: + return text + updated, count = re.subn(pattern, replacement, text, count=1, flags=flags) + if count != 1: + raise SystemExit(f"{label}: expected one match, found {count}") + return updated + + +xaml_path = "MainWindow.xaml" +xaml = read(xaml_path) + +xaml = sub_once( + xaml, + r''' \s*\s*\s*\s*\s*''', + ''' \n \n \n \n ''', + "main grid rows", + re.S) + +if "Compact navigation row: the large top application bar is removed" not in xaml: + pattern = ( + r'''\n \n''' + r''' .*?\n \n\n''' + r''' \n''' + r''' \n \s*''' + r'''\s*''' + r'''\s*''' + r'''\s*''' + r'''\s*''') + replacement = ''' \n \n \n \n \n \n \n \n \n \n \n \n \n ''' + xaml = sub_once(xaml, pattern, replacement, "IED Explorer actions", re.S) + +if xaml.count('Content="{Binding ControlPendingConfirmationLabel}"') != 1: + raise SystemExit("P0 guard: stable single Confirm button is missing") +if 'Content="Confirm Open"' in xaml or 'Content="Confirm Close"' in xaml: + raise SystemExit("P0 guard: legacy dual Confirm buttons detected") +write(xaml_path, xaml) + + +cs_path = "MainWindow.xaml.cs" +cs = read(cs_path) + +if "_autoExpandedCommandDevices" not in cs: + cs = sub_once(cs, r'''( private bool _connectAllInProgress;\n)''', r'''\1 private readonly HashSet _autoExpandedCommandDevices = new(StringComparer.OrdinalIgnoreCase);\n''', "command auto-expand state") + +if "TryAutoExpandCommandPanelOnce(_selectedDevice);" not in cs: + cs = sub_once(cs, r'''( RaiseWorkspaceCounts\(\);\n)( // ctlModel inspection)''', r'''\1 TryAutoExpandCommandPanelOnce(_selectedDevice);\n\2''', "selected-device expansion hook") + +if "SCL model ready. Choose signals" not in cs: + cs = sub_once(cs, r'''( AddLog\("INFO", "SCL", status\);\n)''', r'''\1\n if (firstImported != null && firstImported.Signals.Count > 0)\n {\n AddLog("INFO", "SCL", $"{firstImported.Name}: SCL model ready. Choose signals; saving the selection will continue to endpoint binding, connection, and monitoring.");\n await OpenSignalSelectionWizardAsync(firstImported);\n }\n''', "Open SCL selection workflow") + +if "var allowDynamicReporting = ShouldAllowDynamicReportingForScl(signals);" not in cs: + cs = sub_once(cs, r''' device\.AllowDynamicDataSetWrites = false;\n''', ''' var allowDynamicReporting = ShouldAllowDynamicReportingForScl(signals);\n device.AllowDynamicDataSetWrites = allowDynamicReporting;\n''', "dynamic reporting policy") + +if "Smart Dynamic reporting prepared" not in cs: + pattern = ( + r''' device\.Status = workspace\.RequiresEndpointBinding \? "SCL model ready — bind endpoint" : "SCL model ready";\n''' + r''' device\.Detail = workspace\.RequiresEndpointBinding\n''' + r''' \? "LD/LN/DO/DA are available offline\..*?"\n''' + r''' : "LD/LN/DO/DA were loaded offline\..*?";\n''' + r''' device\.AcquisitionMode = "SCL offline design model";''') + replacement = ''' device.Status = workspace.RequiresEndpointBinding ? "SCL model ready — bind endpoint" : "SCL model ready";\n device.Detail = allowDynamicReporting\n ? (workspace.RequiresEndpointBinding\n ? "LD/LN/DO/DA are available offline. Static report coverage is incomplete; after signal selection and endpoint binding, ArIED will create an association-scoped dynamic DataSet and use a safe free RCB before polling fallback."\n : "LD/LN/DO/DA were loaded offline. Static report coverage is incomplete; ArIED will use static coverage where available and create an association-scoped dynamic DataSet for uncovered selected signals before polling fallback.")\n : (workspace.RequiresEndpointBinding\n ? "LD/LN/DO/DA are available offline. Press Play to bind an MMS endpoint; no discovery traffic was sent while opening the file."\n : "LD/LN/DO/DA were loaded offline. Play performs a fast MMS association; Re-scan performs full design-versus-live verification.");\n device.AcquisitionMode = allowDynamicReporting\n ? "SCL design • Smart Dynamic reporting prepared"\n : "SCL offline design model";''' + cs = sub_once(cs, pattern, replacement, "SCL detail policy", re.S) + +if "IsSmartDynamicCapabilityHint" not in cs: + pattern = r''' private void LogSclFindings\(string sourceName, IReadOnlyList findings\)\n \{.*?\n \}\n\n private bool EnsureSclEndpointBinding''' + replacement = ''' private void LogSclFindings(string sourceName, IReadOnlyList findings)\n {\n var actionableFindings = findings\n .Where(finding => !IsSmartDynamicCapabilityHint(finding))\n .ToArray();\n if (actionableFindings.Length == 0)\n return;\n\n var groups = SclFindingAggregator.Group(actionableFindings);\n if (groups.Count != actionableFindings.Length)\n {\n AddLog(\n "INFO",\n "SCL",\n $"{sourceName} • grouped {actionableFindings.Length} actionable finding(s) into {groups.Count} diagnostic group(s). Full typed evidence remains attached to the SCL workspace.");\n }\n\n foreach (var group in groups.Take(40))\n {\n AddLog(\n SclFindingAggregator.ToLogLevel(group.Severity),\n "SCL",\n $"{sourceName} • {group.Code} [{group.Scope}]: {group.ToDiagnosticMessage()}");\n }\n\n if (groups.Count > 40)\n {\n var omittedRawCount = groups.Skip(40).Sum(group => group.Count);\n AddLog(\n "WARN",\n "SCL",\n $"{groups.Count - 40} additional diagnostic group(s), representing {omittedRawCount} actionable finding(s), were omitted from the live log.");\n }\n\n if (groups.Any(group => SclFindingAggregator.IsBlockingSeverity(group.Severity)))\n MarkDiagnosticAlert();\n }\n\n private static bool IsSmartDynamicCapabilityHint(SclWorkspaceFinding finding)\n {\n if (!finding.Severity.Equals("Warning", StringComparison.OrdinalIgnoreCase))\n return false;\n\n return finding.Code.Equals("SCL_REPORT_DATASET_UNASSIGNED", StringComparison.OrdinalIgnoreCase) ||\n finding.Code.Equals("SCL_REPORT_DATASET_UNRESOLVED", StringComparison.OrdinalIgnoreCase);\n }\n\n private static bool ShouldAllowDynamicReportingForScl(IReadOnlyCollection signals)\n {\n return signals.Any(signal =>\n signal.CanPublishAsSignal &&\n (string.IsNullOrWhiteSpace(signal.DataSetReference) ||\n string.IsNullOrWhiteSpace(signal.ReportControlReference)));\n }\n\n private void TryAutoExpandCommandPanelOnce(Iec61850MonitorDevice? device)\n {\n if (device == null || CommandPanelExpander == null || device.CommandSignals.Count == 0)\n return;\n if (!_autoExpandedCommandDevices.Add(device.DeviceId))\n return;\n\n Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>\n {\n if (ReferenceEquals(SelectedDevice, device) && CommandPanelExpander != null)\n CommandPanelExpander.IsExpanded = true;\n }));\n }\n\n private bool EnsureSclEndpointBinding''' + cs = sub_once(cs, pattern, replacement, "SCL diagnostic policy", re.S) + +if "Saving the selected signals will connect and arm Smart Dynamic reporting" not in cs: + cs = sub_once(cs, r''' device\.Detail = "Endpoint bound locally\. Play will fast-connect from the SCL design model; Re-scan performs full comparison\.";''', ''' device.Detail = device.AllowDynamicDataSetWrites\n ? "Endpoint bound locally. Saving the selected signals will connect and arm Smart Dynamic reporting with a safe free RCB before polling fallback."\n : "Endpoint bound locally. Play will fast-connect from the SCL design model; Re-scan performs full comparison.";''', "endpoint detail") + +if "TryAutoExpandCommandPanelOnce(device);" not in cs: + cs = sub_once(cs, r'''( device\.RefreshCommandSignalProjection\(\);\n RebuildControlFeedbackIndex\(device\);\n)''', r'''\1 TryAutoExpandCommandPanelOnce(device);\n''', "command-ready expansion") + +if "? ShouldAllowDynamicReportingForScl(cachedSignals)" not in cs: + cs = sub_once(cs, r''' AllowDynamicDataSetWrites = hasSclProvenance \? false : profile\.AllowDynamicDataSetWrites,''', ''' AllowDynamicDataSetWrites = hasSclProvenance\n ? ShouldAllowDynamicReportingForScl(cachedSignals)\n : profile.AllowDynamicDataSetWrites,''', "project restore dynamic policy") + +for token in ("TryClaimControlConfirmation", "ControlInspectionBusy", "ControlCommandBusy", "ExecuteClaimedControlAsync"): + if token not in cs: + raise SystemExit(f"P0 guard: missing {token}") +if "SclFindingAggregator.Group(actionableFindings)" not in cs: + raise SystemExit("P1 guard: grouped diagnostics missing") +write(cs_path, cs) + + +project_path = "ArIED61850Tester.csproj" +project = read(project_path) +for old, new in (("1.6.8.0", "1.6.9.0"), ("1.6.8", "1.6.9")): + project = project.replace(old, new) +write(project_path, project) + +workflow_path = ".github/workflows/build.yml" +workflow = read(workflow_path).replace("-Version 1.6.8", "-Version 1.6.9") +write(workflow_path, workflow) + +publish_path = "scripts/publish-windows-portable.ps1" +publish = read(publish_path).replace('"1.6.8"', '"1.6.9"').replace("1.6.8 or v1.6.8", "1.6.9 or v1.6.9") +write(publish_path, publish) + +Path("docs/SCL_SMART_DYNAMIC_REPORTING.md").write_text( + "# SCL-assisted Smart Dynamic reporting\n\n" + "Opening an SCL file remains offline. After the model is loaded, ArIED opens signal selection immediately. Saving a valid live selection continues to endpoint binding, MMS association, and monitoring.\n\n" + "Static DataSet and ReportControl coverage is preferred when complete. Missing or partial coverage enables an association-scoped dynamic DataSet for uncovered selected signals and a safe free RCB before polling fallback.\n\n" + "Unassigned or unresolved report DataSet warnings remain in the typed SCL workspace but are not promoted into noisy live Diagnostics. Positively resolved empty DataSets and other blocking findings remain actionable.\n\n" + "The command panel expands once per IED only after at least one executable command row is ready.\n", + encoding="utf-8") diff --git a/.repair/trigger-final b/.repair/trigger-final new file mode 100644 index 00000000..08a9b401 --- /dev/null +++ b/.repair/trigger-final @@ -0,0 +1 @@ +publish validated repair to fix/scl-workflow-layout-final diff --git a/ArIED61850Tester.csproj b/ArIED61850Tester.csproj index c23d31f4..901e0ebd 100644 --- a/ArIED61850Tester.csproj +++ b/ArIED61850Tester.csproj @@ -14,9 +14,9 @@ ArIED 61850 ArIED 61850 — IEC 61850 Engineering Workstation Windows IEC 61850 engineering workstation for SCL-assisted project setup, live MMS model discovery, independent multi-IED monitoring, reporting diagnostics, sequence of events, and guarded Smart Control. - 1.6.8 - 1.6.8.0 - 1.6.8.0 + 1.6.9 + 1.6.9.0 + 1.6.9.0 iec61850;iec-61850;mms;scl;ied-explorer;multi-ied-monitoring;relay-testing;substation-automation;digital-substation;report-control-block;sequence-of-events;smart-control;wpf;dotnet GPL-3.0-or-later $(ARIEC61850_PROJECT) diff --git a/Assets/app-icon.ico b/Assets/app-icon.ico index c1d489c0..ffadfc32 100644 Binary files a/Assets/app-icon.ico and b/Assets/app-icon.ico differ diff --git a/MainWindow.xaml b/MainWindow.xaml index 4f3b604d..9ba3ceb1 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -8,45 +8,13 @@ Icon="Assets/app-icon.ico" Closing="Window_Closing"> - - - - - - - - - - - - - - - - - - - + @@ -815,7 +799,7 @@ - + diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 223d5a95..a0cee355 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -44,6 +44,7 @@ public partial class MainWindow : Window, INotifyPropertyChanged private bool _hasUnreadDiagnosticError; private bool _signalSelectionWizardOpen; private bool _connectAllInProgress; + private readonly HashSet _autoExpandedCommandDevices = new(StringComparer.OrdinalIgnoreCase); public ObservableCollection Devices { get; } = new(); public BulkObservableCollection GlobalPoints { get; } = new(); @@ -85,6 +86,7 @@ public Iec61850MonitorDevice? SelectedDevice Raise(nameof(ActiveIedTitle)); Raise(nameof(ActiveIedSubtitle)); RaiseWorkspaceCounts(); + TryAutoExpandCommandPanelOnce(_selectedDevice); // ctlModel inspection is preloaded independently of the Expander. Avoid // changing the row set after the panel's first frame has already painted. } @@ -223,6 +225,12 @@ private async void OpenScl_Click(object sender, RoutedEventArgs e) var status = $"{sourceName}: {document.Ieds.Count} IED/AP workspace(s), {offlineCount} offline model(s), {endpointCount} MMS endpoint(s) — {added} added, {refreshed} refreshed, {retained} active retained."; SetStatus(status); AddLog("INFO", "SCL", status); + + if (firstImported != null && firstImported.Signals.Count > 0) + { + AddLog("INFO", "SCL", $"{firstImported.Name}: SCL model ready. Choose signals; saving the selection will continue to endpoint binding, connection, and monitoring."); + await OpenSignalSelectionWizardAsync(firstImported); + } } catch (OperationCanceledException) { @@ -272,7 +280,8 @@ private void ApplySclWorkspaceToDevice( device.Port = 102; } - device.AllowDynamicDataSetWrites = false; + var allowDynamicReporting = ShouldAllowDynamicReportingForScl(signals); + device.AllowDynamicDataSetWrites = allowDynamicReporting; device.SclWorkspace = workspace; device.SclComparison = null; device.SclSourcePath = document.SourcePath; @@ -281,10 +290,16 @@ private void ApplySclWorkspaceToDevice( device.SclAccessPointName = workspace.AccessPointName; device.HasDiscoveryCache = signals.Count > 0; device.Status = workspace.RequiresEndpointBinding ? "SCL model ready — bind endpoint" : "SCL model ready"; - device.Detail = workspace.RequiresEndpointBinding - ? "LD/LN/DO/DA are available offline. Press Play to bind an MMS endpoint; no discovery traffic was sent while opening the file." - : "LD/LN/DO/DA were loaded offline. Play performs a fast MMS association; Re-scan performs full design-versus-live verification."; - device.AcquisitionMode = "SCL offline design model"; + device.Detail = allowDynamicReporting + ? (workspace.RequiresEndpointBinding + ? "LD/LN/DO/DA are available offline. Static report coverage is incomplete; after signal selection and endpoint binding, ArIED will create an association-scoped dynamic DataSet and use a safe free RCB before polling fallback." + : "LD/LN/DO/DA were loaded offline. Static report coverage is incomplete; ArIED will use static coverage where available and create an association-scoped dynamic DataSet for uncovered selected signals before polling fallback.") + : (workspace.RequiresEndpointBinding + ? "LD/LN/DO/DA are available offline. Press Play to bind an MMS endpoint; no discovery traffic was sent while opening the file." + : "LD/LN/DO/DA were loaded offline. Play performs a fast MMS association; Re-scan performs full design-versus-live verification."); + device.AcquisitionMode = allowDynamicReporting + ? "SCL design • Smart Dynamic reporting prepared" + : "SCL offline design model"; foreach (var signal in signals) { @@ -306,16 +321,19 @@ private static string BuildSclWorkspaceSummary(SclIedWorkspace workspace) private void LogSclFindings(string sourceName, IReadOnlyList findings) { - if (findings.Count == 0) + var actionableFindings = findings + .Where(finding => !IsSmartDynamicCapabilityHint(finding)) + .ToArray(); + if (actionableFindings.Length == 0) return; - var groups = SclFindingAggregator.Group(findings); - if (groups.Count != findings.Count) + var groups = SclFindingAggregator.Group(actionableFindings); + if (groups.Count != actionableFindings.Length) { AddLog( "INFO", "SCL", - $"{sourceName} • grouped {findings.Count} raw finding(s) into {groups.Count} diagnostic group(s). Full typed evidence remains attached to the SCL workspace."); + $"{sourceName} • grouped {actionableFindings.Length} actionable finding(s) into {groups.Count} diagnostic group(s). Full typed evidence remains attached to the SCL workspace."); } foreach (var group in groups.Take(40)) @@ -328,19 +346,48 @@ private void LogSclFindings(string sourceName, IReadOnlyList 40) { - var omittedRawCount = groups - .Skip(40) - .Sum(group => group.Count); + var omittedRawCount = groups.Skip(40).Sum(group => group.Count); AddLog( "WARN", "SCL", - $"{groups.Count - 40} additional diagnostic group(s), representing {omittedRawCount} raw finding(s), were omitted from the live log."); + $"{groups.Count - 40} additional diagnostic group(s), representing {omittedRawCount} actionable finding(s), were omitted from the live log."); } if (groups.Any(group => SclFindingAggregator.IsBlockingSeverity(group.Severity))) MarkDiagnosticAlert(); } + private static bool IsSmartDynamicCapabilityHint(SclWorkspaceFinding finding) + { + if (!finding.Severity.Equals("Warning", StringComparison.OrdinalIgnoreCase)) + return false; + + return finding.Code.Equals("SCL_REPORT_DATASET_UNASSIGNED", StringComparison.OrdinalIgnoreCase) || + finding.Code.Equals("SCL_REPORT_DATASET_UNRESOLVED", StringComparison.OrdinalIgnoreCase); + } + + private static bool ShouldAllowDynamicReportingForScl(IReadOnlyCollection signals) + { + return signals.Any(signal => + signal.CanPublishAsSignal && + (string.IsNullOrWhiteSpace(signal.DataSetReference) || + string.IsNullOrWhiteSpace(signal.ReportControlReference))); + } + + private void TryAutoExpandCommandPanelOnce(Iec61850MonitorDevice? device) + { + if (device == null || CommandPanelExpander == null || device.CommandSignals.Count == 0) + return; + if (!_autoExpandedCommandDevices.Add(device.DeviceId)) + return; + + Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => + { + if (ReferenceEquals(SelectedDevice, device) && CommandPanelExpander != null) + CommandPanelExpander.IsExpanded = true; + })); + } + private bool EnsureSclEndpointBinding(Iec61850MonitorDevice device) { if (!device.RequiresEndpointBinding) @@ -357,7 +404,9 @@ private bool EnsureSclEndpointBinding(Iec61850MonitorDevice device) device.IpAddress = wizard.RelayIpAddress; device.Port = wizard.MmsPort; device.Status = "SCL model ready"; - device.Detail = "Endpoint bound locally. Play will fast-connect from the SCL design model; Re-scan performs full comparison."; + device.Detail = device.AllowDynamicDataSetWrites + ? "Endpoint bound locally. Saving the selected signals will connect and arm Smart Dynamic reporting with a safe free RCB before polling fallback." + : "Endpoint bound locally. Play will fast-connect from the SCL design model; Re-scan performs full comparison."; device.RefreshComputed(); NewDeviceIp = device.IpAddress; NewDevicePort = device.Port.ToString(CultureInfo.InvariantCulture); @@ -1024,6 +1073,7 @@ await Task.WhenAll(candidates.Select(async signal => device.RefreshCommandSignalProjection(); RebuildControlFeedbackIndex(device); + TryAutoExpandCommandPanelOnce(device); SetStatus($"{device.Name}: refreshed {candidates.Length} control value(s)."); } @@ -1677,7 +1727,9 @@ private async void OpenProject_Click(object sender, RoutedEventArgs e) LogicalDeviceSummary = profile.LogicalDeviceSummary, IpAddress = profile.IpAddress, Port = profile.Port <= 0 ? 102 : profile.Port, - AllowDynamicDataSetWrites = hasSclProvenance ? false : profile.AllowDynamicDataSetWrites, + AllowDynamicDataSetWrites = hasSclProvenance + ? ShouldAllowDynamicReportingForScl(cachedSignals) + : profile.AllowDynamicDataSetWrites, SclWorkspace = restoredSclWorkspace, SclSourcePath = profile.SclSourcePath, SclSourceSha256 = profile.SclSourceSha256, diff --git a/docs/SCL_SMART_DYNAMIC_REPORTING.md b/docs/SCL_SMART_DYNAMIC_REPORTING.md new file mode 100644 index 00000000..96e8ddc9 --- /dev/null +++ b/docs/SCL_SMART_DYNAMIC_REPORTING.md @@ -0,0 +1,9 @@ +# SCL-assisted Smart Dynamic reporting + +Opening an SCL file remains offline. After the model is loaded, ArIED opens signal selection immediately. Saving a valid live selection continues to endpoint binding, MMS association, and monitoring. + +Static DataSet and ReportControl coverage is preferred when complete. Missing or partial coverage enables an association-scoped dynamic DataSet for uncovered selected signals and a safe free RCB before polling fallback. + +Unassigned or unresolved report DataSet warnings remain in the typed SCL workspace but are not promoted into noisy live Diagnostics. Positively resolved empty DataSets and other blocking findings remain actionable. + +The command panel expands once per IED only after at least one executable command row is ready. diff --git a/scripts/publish-windows-portable.ps1 b/scripts/publish-windows-portable.ps1 index 3e24c65d..b5ec22ff 100644 --- a/scripts/publish-windows-portable.ps1 +++ b/scripts/publish-windows-portable.ps1 @@ -1,5 +1,5 @@ param( - [string]$Version = "1.6.8", + [string]$Version = "1.6.9", [string]$Runtime = "win-x64", [bool]$SingleFile = $true, [bool]$SelfContained = $true, @@ -23,7 +23,7 @@ if ($normalizedVersion.StartsWith("v", [System.StringComparison]::OrdinalIgnoreC $normalizedVersion = $normalizedVersion.Substring(1) } if ($normalizedVersion -notmatch '^\d+\.\d+\.\d+([-.][0-9A-Za-z.-]+)?$') { - throw "Invalid version '$Version'. Use a value such as 1.6.8 or v1.6.8." + throw "Invalid version '$Version'. Use a value such as 1.6.9 or v1.6.9." } $outputRoot = Join-Path $root "dist"