From 025b60a83dfb6fed3c4a77beb7b6793ad504cbca Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 4 Aug 2026 06:59:39 +0700 Subject: [PATCH 1/8] chore: stage IED relay icon QA patch --- .../workflows/ied-relay-icon-self-patch.yml | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 .github/workflows/ied-relay-icon-self-patch.yml diff --git a/.github/workflows/ied-relay-icon-self-patch.yml b/.github/workflows/ied-relay-icon-self-patch.yml new file mode 100644 index 000000000..5eb31d98c --- /dev/null +++ b/.github/workflows/ied-relay-icon-self-patch.yml @@ -0,0 +1,257 @@ +name: Apply IED relay icon industrial QA patch + +on: + push: + branches: + - agent/ied-relay-icon-industrial-qa + +permissions: + contents: write + +jobs: + patch-build-test: + if: github.actor != 'github-actions[bot]' + runs-on: windows-latest + steps: + - name: Checkout ARSAS branch + uses: actions/checkout@v4 + with: + path: arsas + ref: agent/ied-relay-icon-industrial-qa + fetch-depth: 0 + + - name: Checkout ARIEC61850 engine beside ARSAS + uses: actions/checkout@v4 + with: + repository: masarray/ARIEC61850 + path: ARIEC61850 + + - name: Set up .NET 8 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Apply focused XAML and QA updates + shell: bash + working-directory: arsas + run: | + python - <<'PY' + from pathlib import Path + import xml.etree.ElementTree as ET + + def replace_region(path: str, start_token: str, end_token: str, replacement: str) -> None: + file = Path(path) + text = file.read_text(encoding="utf-8") + marker = text.index(start_token) + start = text.rfind("\n", 0, marker) + 1 + end_marker = text.index(end_token, marker) + end = text.rfind("\n", 0, end_marker) + 1 + file.write_text(text[:start] + replacement + text[end:], encoding="utf-8") + + main_block = ''' + + + + + + + + + + + + +''' + replace_region( + "MainWindow.xaml", + "", + ' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +''' + replace_region( + "IoListTestingWindow.xaml", + '', + ' (string?)node.Attribute(x + "Key") == "IedRelayFrontPanelTemplate"); + var namedParts = template + .Descendants() + .Select(node => (string?)node.Attribute(x + "Name")) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Cast() + .ToHashSet(StringComparer.Ordinal); + + Assert.Contains("RelayFasciaArtwork", namedParts); + Assert.Contains("RelayStateRail", namedParts); + Assert.Contains("Assets/ied-protection-relay-fascia.png", template.ToString(), StringComparison.Ordinal); + Assert.True(File.Exists(FindRepoFile("Assets/ied-protection-relay-fascia.png"))); + + var project = File.ReadAllText(FindRepoFile("ArIED61850Tester.csproj")); + Assert.Contains("Assets\\\\ied-protection-relay-fascia.png", project, StringComparison.Ordinal); + + var explorer = XDocument.Load(FindRepoFile("MainWindow.xaml")); + var explorerIcon = explorer + .Descendants(presentation + "Control") + .Single(node => (string?)node.Attribute(x + "Name") == "RelayDeviceIcon"); + var explorerBadge = explorer + .Descendants(presentation + "Border") + .Single(node => (string?)node.Attribute(x + "Name") == "MonitorStateBadge"); + Assert.Empty(explorerIcon.Descendants(presentation + "DropShadowEffect")); + Assert.Equal("StackPanel", explorerBadge.Parent?.Name.LocalName); + Assert.Contains( + explorerBadge.Parent!.Descendants(presentation + "Control"), + node => (string?)node.Attribute(x + "Name") == "RelayDeviceIcon"); + + var ioTesting = XDocument.Load(FindRepoFile("IoListTestingWindow.xaml")); + var ioBadge = ioTesting + .Descendants(presentation + "Border") + .Single(node => (string?)node.Attribute(x + "Name") == "StateBadge"); + Assert.Equal("StackPanel", ioBadge.Parent?.Name.LocalName); + Assert.Contains( + ioBadge.Parent!.Descendants(presentation + "Control"), + node => (string?)node.Attribute(x + "Name") == "RelayIcon"); + + var explorerText = explorer.ToString(); + var ioTestingText = ioTesting.ToString(); + Assert.Contains("IedRelayFrontPanelTemplate", explorerText, StringComparison.Ordinal); + Assert.Contains("IedRelayFrontPanelTemplate", ioTestingText, StringComparison.Ordinal); + Assert.DoesNotContain("M 2 0 L 2 20", explorerText, StringComparison.Ordinal); + Assert.DoesNotContain("M 2 0 L 2 20", ioTestingText, StringComparison.Ordinal); + } + +''' + tests.write_text(test_text[:test_start] + test_method + test_text[test_end:], encoding="utf-8") + + Path("design-qa.md").write_text('''# IED protection relay fascia design QA + +## Scope + +- Source artwork: `Assets/ied-protection-relay-fascia.png` +- Reusable WPF consumer: `IedRelayFrontPanelTemplate` in `App.xaml` +- Runtime surfaces: IED Explorer (`MainWindow.xaml`) and IO List FAT (`IoListTestingWindow.xaml`) +- Production icon size: 50 × 50 device-independent pixels + +## Implementation checks + +- The dedicated relay fascia PNG is packaged as a WPF resource and reused on both IED card surfaces. +- The former calculator-style inline path is absent from both views. +- LIVE/STOP state remains data-driven and is rendered as a compact label below the relay artwork. +- The state label is no longer layered over the fascia or LCD. +- The IED Explorer icon has no `DropShadowEffect`; status is conveyed by the restrained label and state rail only. +- IO List FAT keeps the final `✔ PASS` result badge in the text area while operational LIVE/STOP state stays below the icon. + +## Validation + +The patch branch is committed only after these Windows checks pass: + +```powershell +dotnet build ArIED61850Tester.csproj -c Release +dotnet test tests/ARSAS.Tests/ARSAS.Tests.csproj -c Release --filter "FullyQualifiedName~IoTestingUiContractTests" +``` + +The focused UI contract suite contains 11 tests, including structural checks that both card surfaces use the shared fascia, keep status labels outside the artwork, and do not restore the old calculator path. + +## Visual QA status + +The source asset itself is repository-visible and auditable. A fresh full-window runtime screenshot remains recommended as P3 evidence for spacing at every Windows scaling factor; it is not represented by inaccessible machine-local paths in this document. + +Final result: code structure and UI contracts passed; runtime screenshot evidence remains a follow-up polish item. +''', encoding="utf-8") + + ET.parse("MainWindow.xaml") + ET.parse("IoListTestingWindow.xaml") + print("XAML patch and XML parse checks passed") + PY + + - name: Build ARSAS Release + shell: pwsh + working-directory: arsas + run: dotnet build ArIED61850Tester.csproj -c Release + + - name: Run 11 focused UI contracts + shell: pwsh + working-directory: arsas + run: dotnet test tests/ARSAS.Tests/ARSAS.Tests.csproj -c Release --filter "FullyQualifiedName~IoTestingUiContractTests" + + - name: Remove one-shot helper and commit validated patch + shell: pwsh + working-directory: arsas + run: | + Remove-Item ".github/workflows/ied-relay-icon-self-patch.yml" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add MainWindow.xaml IoListTestingWindow.xaml tests/ARSAS.Tests/IoTestingUiContractTests.cs design-qa.md .github/workflows/ied-relay-icon-self-patch.yml + git commit -m "fix: place IED status below relay fascia" + git push origin HEAD:agent/ied-relay-icon-industrial-qa From d0681c3510982fa47858a15b07dc49e10f04d5a1 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 4 Aug 2026 07:01:38 +0700 Subject: [PATCH 2/8] chore: add one-shot IED relay patch script --- .github/scripts/apply_ied_relay_patch.py | 199 +++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 .github/scripts/apply_ied_relay_patch.py diff --git a/.github/scripts/apply_ied_relay_patch.py b/.github/scripts/apply_ied_relay_patch.py new file mode 100644 index 000000000..b29a0e6cf --- /dev/null +++ b/.github/scripts/apply_ied_relay_patch.py @@ -0,0 +1,199 @@ +from pathlib import Path +import xml.etree.ElementTree as ET + + +def replace_region(path: str, start_token: str, end_token: str, replacement: str) -> None: + file = Path(path) + text = file.read_text(encoding="utf-8") + marker = text.index(start_token) + start = text.rfind("\n", 0, marker) + 1 + end_marker = text.index(end_token, marker) + end = text.rfind("\n", 0, end_marker) + 1 + file.write_text(text[:start] + replacement + text[end:], encoding="utf-8") + + +main_block = ''' + + + + + + + + + + + + +''' +replace_region( + "MainWindow.xaml", + "", + ' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +''' +replace_region( + "IoListTestingWindow.xaml", + '', + ' (string?)node.Attribute(x + "Key") == "IedRelayFrontPanelTemplate"); + var namedParts = template + .Descendants() + .Select(node => (string?)node.Attribute(x + "Name")) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Cast() + .ToHashSet(StringComparer.Ordinal); + + Assert.Contains("RelayFasciaArtwork", namedParts); + Assert.Contains("RelayStateRail", namedParts); + Assert.Contains("Assets/ied-protection-relay-fascia.png", template.ToString(), StringComparison.Ordinal); + Assert.True(File.Exists(FindRepoFile("Assets/ied-protection-relay-fascia.png"))); + + var project = File.ReadAllText(FindRepoFile("ArIED61850Tester.csproj")); + Assert.Contains("Assets\\\\ied-protection-relay-fascia.png", project, StringComparison.Ordinal); + + var explorer = XDocument.Load(FindRepoFile("MainWindow.xaml")); + var explorerIcon = explorer + .Descendants(presentation + "Control") + .Single(node => (string?)node.Attribute(x + "Name") == "RelayDeviceIcon"); + var explorerBadge = explorer + .Descendants(presentation + "Border") + .Single(node => (string?)node.Attribute(x + "Name") == "MonitorStateBadge"); + Assert.Empty(explorerIcon.Descendants(presentation + "DropShadowEffect")); + Assert.Equal("StackPanel", explorerBadge.Parent?.Name.LocalName); + Assert.Contains( + explorerBadge.Parent!.Descendants(presentation + "Control"), + node => (string?)node.Attribute(x + "Name") == "RelayDeviceIcon"); + + var ioTesting = XDocument.Load(FindRepoFile("IoListTestingWindow.xaml")); + var ioBadge = ioTesting + .Descendants(presentation + "Border") + .Single(node => (string?)node.Attribute(x + "Name") == "StateBadge"); + Assert.Equal("StackPanel", ioBadge.Parent?.Name.LocalName); + Assert.Contains( + ioBadge.Parent!.Descendants(presentation + "Control"), + node => (string?)node.Attribute(x + "Name") == "RelayIcon"); + + var explorerText = explorer.ToString(); + var ioTestingText = ioTesting.ToString(); + Assert.Contains("IedRelayFrontPanelTemplate", explorerText, StringComparison.Ordinal); + Assert.Contains("IedRelayFrontPanelTemplate", ioTestingText, StringComparison.Ordinal); + Assert.DoesNotContain("M 2 0 L 2 20", explorerText, StringComparison.Ordinal); + Assert.DoesNotContain("M 2 0 L 2 20", ioTestingText, StringComparison.Ordinal); + } + +''' +tests.write_text(test_text[:test_start] + test_method + test_text[test_end:], encoding="utf-8") + +Path("design-qa.md").write_text('''# IED protection relay fascia design QA + +## Scope + +- Source artwork: `Assets/ied-protection-relay-fascia.png` +- Reusable WPF consumer: `IedRelayFrontPanelTemplate` in `App.xaml` +- Runtime surfaces: IED Explorer (`MainWindow.xaml`) and IO List FAT (`IoListTestingWindow.xaml`) +- Production icon size: 50 × 50 device-independent pixels + +## Implementation checks + +- The dedicated relay fascia PNG is packaged as a WPF resource and reused on both IED card surfaces. +- The former calculator-style inline path is absent from both views. +- LIVE/STOP state remains data-driven and is rendered as a compact label below the relay artwork. +- The state label is no longer layered over the fascia or LCD. +- The IED Explorer icon has no `DropShadowEffect`; status is conveyed by the restrained label and state rail only. +- IO List FAT keeps the final `✔ PASS` result badge in the text area while operational LIVE/STOP state stays below the icon. + +## Validation + +The patch branch is committed only after these Windows checks pass: + +```powershell +dotnet build ArIED61850Tester.csproj -c Release +dotnet test tests/ARSAS.Tests/ARSAS.Tests.csproj -c Release --filter "FullyQualifiedName~IoTestingUiContractTests" +``` + +The focused UI contract suite contains 11 tests, including structural checks that both card surfaces use the shared fascia, keep status labels outside the artwork, and do not restore the old calculator path. + +## Visual QA status + +The source asset itself is repository-visible and auditable. A fresh full-window runtime screenshot remains recommended as P3 evidence for spacing at every Windows scaling factor; it is not represented by inaccessible machine-local paths in this document. + +Final result: code structure and UI contracts passed; runtime screenshot evidence remains a follow-up polish item. +''', encoding="utf-8") + +ET.parse("MainWindow.xaml") +ET.parse("IoListTestingWindow.xaml") +print("IED relay patch and XML parse checks passed") From 033d29f73daba0666b743c626b31b9201bb3b73a Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 4 Aug 2026 07:01:57 +0700 Subject: [PATCH 3/8] chore: validate IED relay patch on pull request --- .github/workflows/ied-relay-icon-pr-patch.yml | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/ied-relay-icon-pr-patch.yml diff --git a/.github/workflows/ied-relay-icon-pr-patch.yml b/.github/workflows/ied-relay-icon-pr-patch.yml new file mode 100644 index 000000000..d467e55dd --- /dev/null +++ b/.github/workflows/ied-relay-icon-pr-patch.yml @@ -0,0 +1,61 @@ +name: Validate and apply IED relay icon patch + +on: + pull_request: + branches: + - main + types: [opened, synchronize, reopened] + +permissions: + contents: write + +jobs: + patch-build-test: + if: github.event.pull_request.head.ref == 'agent/ied-relay-icon-industrial-qa' && github.actor != 'github-actions[bot]' + runs-on: windows-latest + steps: + - name: Checkout ARSAS head branch + uses: actions/checkout@v4 + with: + path: arsas + ref: agent/ied-relay-icon-industrial-qa + fetch-depth: 0 + + - name: Checkout ARIEC61850 engine beside ARSAS + uses: actions/checkout@v4 + with: + repository: masarray/ARIEC61850 + path: ARIEC61850 + + - name: Set up .NET 8 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Apply focused patch + shell: pwsh + working-directory: arsas + run: python .github/scripts/apply_ied_relay_patch.py + + - name: Build ARSAS Release + shell: pwsh + working-directory: arsas + run: dotnet build ArIED61850Tester.csproj -c Release + + - name: Run 11 focused UI contracts + shell: pwsh + working-directory: arsas + run: dotnet test tests/ARSAS.Tests/ARSAS.Tests.csproj -c Release --filter "FullyQualifiedName~IoTestingUiContractTests" + + - name: Commit validated source changes + shell: pwsh + working-directory: arsas + run: | + Remove-Item ".github/workflows/ied-relay-icon-self-patch.yml" + Remove-Item ".github/workflows/ied-relay-icon-pr-patch.yml" + Remove-Item ".github/scripts/apply_ied_relay_patch.py" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add MainWindow.xaml IoListTestingWindow.xaml tests/ARSAS.Tests/IoTestingUiContractTests.cs design-qa.md .github + git commit -m "fix: place IED status below relay fascia" + git push origin HEAD:agent/ied-relay-icon-industrial-qa From 563ebfa538bed7829c25668bcb26740d6c02a8b0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 00:03:35 +0000 Subject: [PATCH 4/8] fix: place IED status below relay fascia --- .github/scripts/apply_ied_relay_patch.py | 199 -------------- .github/workflows/ied-relay-icon-pr-patch.yml | 61 ----- .../workflows/ied-relay-icon-self-patch.yml | 257 ------------------ IoListTestingWindow.xaml | 57 ++-- MainWindow.xaml | 43 +-- design-qa.md | 55 ++-- tests/ARSAS.Tests/IoTestingUiContractTests.cs | 34 ++- 7 files changed, 102 insertions(+), 604 deletions(-) delete mode 100644 .github/scripts/apply_ied_relay_patch.py delete mode 100644 .github/workflows/ied-relay-icon-pr-patch.yml delete mode 100644 .github/workflows/ied-relay-icon-self-patch.yml diff --git a/.github/scripts/apply_ied_relay_patch.py b/.github/scripts/apply_ied_relay_patch.py deleted file mode 100644 index b29a0e6cf..000000000 --- a/.github/scripts/apply_ied_relay_patch.py +++ /dev/null @@ -1,199 +0,0 @@ -from pathlib import Path -import xml.etree.ElementTree as ET - - -def replace_region(path: str, start_token: str, end_token: str, replacement: str) -> None: - file = Path(path) - text = file.read_text(encoding="utf-8") - marker = text.index(start_token) - start = text.rfind("\n", 0, marker) + 1 - end_marker = text.index(end_token, marker) - end = text.rfind("\n", 0, end_marker) + 1 - file.write_text(text[:start] + replacement + text[end:], encoding="utf-8") - - -main_block = ''' - - - - - - - - - - - - -''' -replace_region( - "MainWindow.xaml", - "", - ' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -''' -replace_region( - "IoListTestingWindow.xaml", - '', - ' (string?)node.Attribute(x + "Key") == "IedRelayFrontPanelTemplate"); - var namedParts = template - .Descendants() - .Select(node => (string?)node.Attribute(x + "Name")) - .Where(name => !string.IsNullOrWhiteSpace(name)) - .Cast() - .ToHashSet(StringComparer.Ordinal); - - Assert.Contains("RelayFasciaArtwork", namedParts); - Assert.Contains("RelayStateRail", namedParts); - Assert.Contains("Assets/ied-protection-relay-fascia.png", template.ToString(), StringComparison.Ordinal); - Assert.True(File.Exists(FindRepoFile("Assets/ied-protection-relay-fascia.png"))); - - var project = File.ReadAllText(FindRepoFile("ArIED61850Tester.csproj")); - Assert.Contains("Assets\\\\ied-protection-relay-fascia.png", project, StringComparison.Ordinal); - - var explorer = XDocument.Load(FindRepoFile("MainWindow.xaml")); - var explorerIcon = explorer - .Descendants(presentation + "Control") - .Single(node => (string?)node.Attribute(x + "Name") == "RelayDeviceIcon"); - var explorerBadge = explorer - .Descendants(presentation + "Border") - .Single(node => (string?)node.Attribute(x + "Name") == "MonitorStateBadge"); - Assert.Empty(explorerIcon.Descendants(presentation + "DropShadowEffect")); - Assert.Equal("StackPanel", explorerBadge.Parent?.Name.LocalName); - Assert.Contains( - explorerBadge.Parent!.Descendants(presentation + "Control"), - node => (string?)node.Attribute(x + "Name") == "RelayDeviceIcon"); - - var ioTesting = XDocument.Load(FindRepoFile("IoListTestingWindow.xaml")); - var ioBadge = ioTesting - .Descendants(presentation + "Border") - .Single(node => (string?)node.Attribute(x + "Name") == "StateBadge"); - Assert.Equal("StackPanel", ioBadge.Parent?.Name.LocalName); - Assert.Contains( - ioBadge.Parent!.Descendants(presentation + "Control"), - node => (string?)node.Attribute(x + "Name") == "RelayIcon"); - - var explorerText = explorer.ToString(); - var ioTestingText = ioTesting.ToString(); - Assert.Contains("IedRelayFrontPanelTemplate", explorerText, StringComparison.Ordinal); - Assert.Contains("IedRelayFrontPanelTemplate", ioTestingText, StringComparison.Ordinal); - Assert.DoesNotContain("M 2 0 L 2 20", explorerText, StringComparison.Ordinal); - Assert.DoesNotContain("M 2 0 L 2 20", ioTestingText, StringComparison.Ordinal); - } - -''' -tests.write_text(test_text[:test_start] + test_method + test_text[test_end:], encoding="utf-8") - -Path("design-qa.md").write_text('''# IED protection relay fascia design QA - -## Scope - -- Source artwork: `Assets/ied-protection-relay-fascia.png` -- Reusable WPF consumer: `IedRelayFrontPanelTemplate` in `App.xaml` -- Runtime surfaces: IED Explorer (`MainWindow.xaml`) and IO List FAT (`IoListTestingWindow.xaml`) -- Production icon size: 50 × 50 device-independent pixels - -## Implementation checks - -- The dedicated relay fascia PNG is packaged as a WPF resource and reused on both IED card surfaces. -- The former calculator-style inline path is absent from both views. -- LIVE/STOP state remains data-driven and is rendered as a compact label below the relay artwork. -- The state label is no longer layered over the fascia or LCD. -- The IED Explorer icon has no `DropShadowEffect`; status is conveyed by the restrained label and state rail only. -- IO List FAT keeps the final `✔ PASS` result badge in the text area while operational LIVE/STOP state stays below the icon. - -## Validation - -The patch branch is committed only after these Windows checks pass: - -```powershell -dotnet build ArIED61850Tester.csproj -c Release -dotnet test tests/ARSAS.Tests/ARSAS.Tests.csproj -c Release --filter "FullyQualifiedName~IoTestingUiContractTests" -``` - -The focused UI contract suite contains 11 tests, including structural checks that both card surfaces use the shared fascia, keep status labels outside the artwork, and do not restore the old calculator path. - -## Visual QA status - -The source asset itself is repository-visible and auditable. A fresh full-window runtime screenshot remains recommended as P3 evidence for spacing at every Windows scaling factor; it is not represented by inaccessible machine-local paths in this document. - -Final result: code structure and UI contracts passed; runtime screenshot evidence remains a follow-up polish item. -''', encoding="utf-8") - -ET.parse("MainWindow.xaml") -ET.parse("IoListTestingWindow.xaml") -print("IED relay patch and XML parse checks passed") diff --git a/.github/workflows/ied-relay-icon-pr-patch.yml b/.github/workflows/ied-relay-icon-pr-patch.yml deleted file mode 100644 index d467e55dd..000000000 --- a/.github/workflows/ied-relay-icon-pr-patch.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Validate and apply IED relay icon patch - -on: - pull_request: - branches: - - main - types: [opened, synchronize, reopened] - -permissions: - contents: write - -jobs: - patch-build-test: - if: github.event.pull_request.head.ref == 'agent/ied-relay-icon-industrial-qa' && github.actor != 'github-actions[bot]' - runs-on: windows-latest - steps: - - name: Checkout ARSAS head branch - uses: actions/checkout@v4 - with: - path: arsas - ref: agent/ied-relay-icon-industrial-qa - fetch-depth: 0 - - - name: Checkout ARIEC61850 engine beside ARSAS - uses: actions/checkout@v4 - with: - repository: masarray/ARIEC61850 - path: ARIEC61850 - - - name: Set up .NET 8 - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - - name: Apply focused patch - shell: pwsh - working-directory: arsas - run: python .github/scripts/apply_ied_relay_patch.py - - - name: Build ARSAS Release - shell: pwsh - working-directory: arsas - run: dotnet build ArIED61850Tester.csproj -c Release - - - name: Run 11 focused UI contracts - shell: pwsh - working-directory: arsas - run: dotnet test tests/ARSAS.Tests/ARSAS.Tests.csproj -c Release --filter "FullyQualifiedName~IoTestingUiContractTests" - - - name: Commit validated source changes - shell: pwsh - working-directory: arsas - run: | - Remove-Item ".github/workflows/ied-relay-icon-self-patch.yml" - Remove-Item ".github/workflows/ied-relay-icon-pr-patch.yml" - Remove-Item ".github/scripts/apply_ied_relay_patch.py" - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add MainWindow.xaml IoListTestingWindow.xaml tests/ARSAS.Tests/IoTestingUiContractTests.cs design-qa.md .github - git commit -m "fix: place IED status below relay fascia" - git push origin HEAD:agent/ied-relay-icon-industrial-qa diff --git a/.github/workflows/ied-relay-icon-self-patch.yml b/.github/workflows/ied-relay-icon-self-patch.yml deleted file mode 100644 index 5eb31d98c..000000000 --- a/.github/workflows/ied-relay-icon-self-patch.yml +++ /dev/null @@ -1,257 +0,0 @@ -name: Apply IED relay icon industrial QA patch - -on: - push: - branches: - - agent/ied-relay-icon-industrial-qa - -permissions: - contents: write - -jobs: - patch-build-test: - if: github.actor != 'github-actions[bot]' - runs-on: windows-latest - steps: - - name: Checkout ARSAS branch - uses: actions/checkout@v4 - with: - path: arsas - ref: agent/ied-relay-icon-industrial-qa - fetch-depth: 0 - - - name: Checkout ARIEC61850 engine beside ARSAS - uses: actions/checkout@v4 - with: - repository: masarray/ARIEC61850 - path: ARIEC61850 - - - name: Set up .NET 8 - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - - name: Apply focused XAML and QA updates - shell: bash - working-directory: arsas - run: | - python - <<'PY' - from pathlib import Path - import xml.etree.ElementTree as ET - - def replace_region(path: str, start_token: str, end_token: str, replacement: str) -> None: - file = Path(path) - text = file.read_text(encoding="utf-8") - marker = text.index(start_token) - start = text.rfind("\n", 0, marker) + 1 - end_marker = text.index(end_token, marker) - end = text.rfind("\n", 0, end_marker) + 1 - file.write_text(text[:start] + replacement + text[end:], encoding="utf-8") - - main_block = ''' - - - - - - - - - - - - -''' - replace_region( - "MainWindow.xaml", - "", - ' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -''' - replace_region( - "IoListTestingWindow.xaml", - '', - ' (string?)node.Attribute(x + "Key") == "IedRelayFrontPanelTemplate"); - var namedParts = template - .Descendants() - .Select(node => (string?)node.Attribute(x + "Name")) - .Where(name => !string.IsNullOrWhiteSpace(name)) - .Cast() - .ToHashSet(StringComparer.Ordinal); - - Assert.Contains("RelayFasciaArtwork", namedParts); - Assert.Contains("RelayStateRail", namedParts); - Assert.Contains("Assets/ied-protection-relay-fascia.png", template.ToString(), StringComparison.Ordinal); - Assert.True(File.Exists(FindRepoFile("Assets/ied-protection-relay-fascia.png"))); - - var project = File.ReadAllText(FindRepoFile("ArIED61850Tester.csproj")); - Assert.Contains("Assets\\\\ied-protection-relay-fascia.png", project, StringComparison.Ordinal); - - var explorer = XDocument.Load(FindRepoFile("MainWindow.xaml")); - var explorerIcon = explorer - .Descendants(presentation + "Control") - .Single(node => (string?)node.Attribute(x + "Name") == "RelayDeviceIcon"); - var explorerBadge = explorer - .Descendants(presentation + "Border") - .Single(node => (string?)node.Attribute(x + "Name") == "MonitorStateBadge"); - Assert.Empty(explorerIcon.Descendants(presentation + "DropShadowEffect")); - Assert.Equal("StackPanel", explorerBadge.Parent?.Name.LocalName); - Assert.Contains( - explorerBadge.Parent!.Descendants(presentation + "Control"), - node => (string?)node.Attribute(x + "Name") == "RelayDeviceIcon"); - - var ioTesting = XDocument.Load(FindRepoFile("IoListTestingWindow.xaml")); - var ioBadge = ioTesting - .Descendants(presentation + "Border") - .Single(node => (string?)node.Attribute(x + "Name") == "StateBadge"); - Assert.Equal("StackPanel", ioBadge.Parent?.Name.LocalName); - Assert.Contains( - ioBadge.Parent!.Descendants(presentation + "Control"), - node => (string?)node.Attribute(x + "Name") == "RelayIcon"); - - var explorerText = explorer.ToString(); - var ioTestingText = ioTesting.ToString(); - Assert.Contains("IedRelayFrontPanelTemplate", explorerText, StringComparison.Ordinal); - Assert.Contains("IedRelayFrontPanelTemplate", ioTestingText, StringComparison.Ordinal); - Assert.DoesNotContain("M 2 0 L 2 20", explorerText, StringComparison.Ordinal); - Assert.DoesNotContain("M 2 0 L 2 20", ioTestingText, StringComparison.Ordinal); - } - -''' - tests.write_text(test_text[:test_start] + test_method + test_text[test_end:], encoding="utf-8") - - Path("design-qa.md").write_text('''# IED protection relay fascia design QA - -## Scope - -- Source artwork: `Assets/ied-protection-relay-fascia.png` -- Reusable WPF consumer: `IedRelayFrontPanelTemplate` in `App.xaml` -- Runtime surfaces: IED Explorer (`MainWindow.xaml`) and IO List FAT (`IoListTestingWindow.xaml`) -- Production icon size: 50 × 50 device-independent pixels - -## Implementation checks - -- The dedicated relay fascia PNG is packaged as a WPF resource and reused on both IED card surfaces. -- The former calculator-style inline path is absent from both views. -- LIVE/STOP state remains data-driven and is rendered as a compact label below the relay artwork. -- The state label is no longer layered over the fascia or LCD. -- The IED Explorer icon has no `DropShadowEffect`; status is conveyed by the restrained label and state rail only. -- IO List FAT keeps the final `✔ PASS` result badge in the text area while operational LIVE/STOP state stays below the icon. - -## Validation - -The patch branch is committed only after these Windows checks pass: - -```powershell -dotnet build ArIED61850Tester.csproj -c Release -dotnet test tests/ARSAS.Tests/ARSAS.Tests.csproj -c Release --filter "FullyQualifiedName~IoTestingUiContractTests" -``` - -The focused UI contract suite contains 11 tests, including structural checks that both card surfaces use the shared fascia, keep status labels outside the artwork, and do not restore the old calculator path. - -## Visual QA status - -The source asset itself is repository-visible and auditable. A fresh full-window runtime screenshot remains recommended as P3 evidence for spacing at every Windows scaling factor; it is not represented by inaccessible machine-local paths in this document. - -Final result: code structure and UI contracts passed; runtime screenshot evidence remains a follow-up polish item. -''', encoding="utf-8") - - ET.parse("MainWindow.xaml") - ET.parse("IoListTestingWindow.xaml") - print("XAML patch and XML parse checks passed") - PY - - - name: Build ARSAS Release - shell: pwsh - working-directory: arsas - run: dotnet build ArIED61850Tester.csproj -c Release - - - name: Run 11 focused UI contracts - shell: pwsh - working-directory: arsas - run: dotnet test tests/ARSAS.Tests/ARSAS.Tests.csproj -c Release --filter "FullyQualifiedName~IoTestingUiContractTests" - - - name: Remove one-shot helper and commit validated patch - shell: pwsh - working-directory: arsas - run: | - Remove-Item ".github/workflows/ied-relay-icon-self-patch.yml" - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add MainWindow.xaml IoListTestingWindow.xaml tests/ARSAS.Tests/IoTestingUiContractTests.cs design-qa.md .github/workflows/ied-relay-icon-self-patch.yml - git commit -m "fix: place IED status below relay fascia" - git push origin HEAD:agent/ied-relay-icon-industrial-qa diff --git a/IoListTestingWindow.xaml b/IoListTestingWindow.xaml index 8cc73a626..f62ba6396 100644 --- a/IoListTestingWindow.xaml +++ b/IoListTestingWindow.xaml @@ -166,10 +166,25 @@ - - - + + + + + + + + + + + + + + @@ -185,29 +200,17 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + diff --git a/MainWindow.xaml b/MainWindow.xaml index b0b6159d5..d9c8cbea2 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -229,27 +229,28 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + (string?)node.Attribute(x + "Name") == "RelayDeviceIcon"); + var explorerBadge = explorer + .Descendants(presentation + "Border") + .Single(node => (string?)node.Attribute(x + "Name") == "MonitorStateBadge"); + Assert.Empty(explorerIcon.Descendants(presentation + "DropShadowEffect")); + Assert.Equal("StackPanel", explorerBadge.Parent?.Name.LocalName); + Assert.Contains( + explorerBadge.Parent!.Descendants(presentation + "Control"), + node => (string?)node.Attribute(x + "Name") == "RelayDeviceIcon"); + + var ioTesting = XDocument.Load(FindRepoFile("IoListTestingWindow.xaml")); + var ioBadge = ioTesting + .Descendants(presentation + "Border") + .Single(node => (string?)node.Attribute(x + "Name") == "StateBadge"); + Assert.Equal("StackPanel", ioBadge.Parent?.Name.LocalName); + Assert.Contains( + ioBadge.Parent!.Descendants(presentation + "Control"), + node => (string?)node.Attribute(x + "Name") == "RelayIcon"); + + var explorerText = explorer.ToString(); + var ioTestingText = ioTesting.ToString(); + Assert.Contains("IedRelayFrontPanelTemplate", explorerText, StringComparison.Ordinal); + Assert.Contains("IedRelayFrontPanelTemplate", ioTestingText, StringComparison.Ordinal); + Assert.DoesNotContain("M 2 0 L 2 20", explorerText, StringComparison.Ordinal); + Assert.DoesNotContain("M 2 0 L 2 20", ioTestingText, StringComparison.Ordinal); } [Fact] From 3d7e2a7c4f27f1fc69eb3fdd4f88fb7f89676c94 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 4 Aug 2026 07:05:05 +0700 Subject: [PATCH 5/8] docs: record verified IED relay validation --- design-qa.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/design-qa.md b/design-qa.md index 6513fae3e..547ac2406 100644 --- a/design-qa.md +++ b/design-qa.md @@ -18,17 +18,23 @@ ## Validation -The patch branch is committed only after these Windows checks pass: +Verified on GitHub Actions using Windows Server 2025 and .NET SDK 8.0.423: ```powershell dotnet build ArIED61850Tester.csproj -c Release dotnet test tests/ARSAS.Tests/ARSAS.Tests.csproj -c Release --filter "FullyQualifiedName~IoTestingUiContractTests" ``` -The focused UI contract suite contains 11 tests, including structural checks that both card surfaces use the shared fascia, keep status labels outside the artwork, and do not restore the old calculator path. +Results: + +- Release build: passed with 0 errors. The 12 reported nullable-reference warnings pre-existed in unrelated connection-classifier and signal-viewer files. +- Focused UI contracts: 11 passed, 0 failed, 0 skipped. +- XAML parse checks: passed for `MainWindow.xaml` and `IoListTestingWindow.xaml`. + +The focused UI contract suite includes structural checks that both card surfaces use the shared fascia, keep status labels outside the artwork, remove the icon-level shadow, and do not restore the old calculator path. ## Visual QA status The source asset itself is repository-visible and auditable. A fresh full-window runtime screenshot remains recommended as P3 evidence for spacing at every Windows scaling factor; it is not represented by inaccessible machine-local paths in this document. -Final result: code structure and UI contracts passed; runtime screenshot evidence remains a follow-up polish item. +Final result: code structure, Release build, and 11/11 UI contracts passed; runtime screenshot evidence remains a follow-up polish item. From 95026d647e8d7a8e1ee26cbb5e417614c13d474f Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 4 Aug 2026 07:08:51 +0700 Subject: [PATCH 6/8] chore: align build invariant with relay fascia layout --- .github/workflows/ied-relay-ci-guard-fix.yml | 60 ++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 .github/workflows/ied-relay-ci-guard-fix.yml diff --git a/.github/workflows/ied-relay-ci-guard-fix.yml b/.github/workflows/ied-relay-ci-guard-fix.yml new file mode 100644 index 000000000..0b79cfa1d --- /dev/null +++ b/.github/workflows/ied-relay-ci-guard-fix.yml @@ -0,0 +1,60 @@ +name: Align IED relay build invariant + +on: + pull_request: + branches: + - main + types: [opened, synchronize, reopened] + +permissions: + contents: write + +jobs: + patch-build-guard: + if: github.event.pull_request.head.ref == 'agent/ied-relay-icon-industrial-qa' && github.actor != 'github-actions[bot]' + runs-on: windows-latest + steps: + - name: Checkout branch + uses: actions/checkout@v4 + with: + ref: agent/ied-relay-icon-industrial-qa + fetch-depth: 0 + + - name: Update relay-icon invariant + shell: pwsh + run: | + $path = '.github/workflows/build.yml' + $text = Get-Content $path -Raw + $old = @' + $main -notmatch '' -or +'@ + if (-not $text.Contains($old)) { + throw 'The legacy 44x44 relay invariant was not found.' + } + $text = $text.Replace($old, $new) + Set-Content -Path $path -Value $text -Encoding utf8 + + - name: Verify updated guard against current source + shell: pwsh + run: | + $main = Get-Content MainWindow.xaml -Raw + if ($main -notmatch '') { + throw 'The new industrial relay layout does not satisfy the updated invariant.' + } + + - name: Commit guard update + shell: pwsh + run: | + Remove-Item '.github/workflows/ied-relay-ci-guard-fix.yml' + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add .github/workflows/build.yml .github/workflows/ied-relay-ci-guard-fix.yml + git commit -m 'ci: align relay icon visual invariant' + git push origin HEAD:agent/ied-relay-icon-industrial-qa From 9f6062b631a430170484dacb51fcc24085407746 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 4 Aug 2026 07:10:59 +0700 Subject: [PATCH 7/8] ci: align relay icon visual invariant --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b7c2ede74..da1e272f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -101,7 +101,9 @@ jobs: $main -notmatch 'Header="Flags"' -or $main -notmatch 'Header="Diagnostics" Binding="\{Binding DiagnosticsSummary\}"' -or $main -notmatch '' -or - $main -notmatch '' -or $main -notmatch 'LucideSquare.*Stroke="\{StaticResource Warning\}"' -or $main -notmatch 'LucideX.*Stroke="\{StaticResource Danger\}"' -or $app -notmatch 'x:Key="CommandOpenGradient"' -or From 98437bb5ccd262349f81813e8367dcf812b26ee1 Mon Sep 17 00:00:00 2001 From: masarray Date: Tue, 4 Aug 2026 07:11:07 +0700 Subject: [PATCH 8/8] chore: remove temporary relay CI helper --- .github/workflows/ied-relay-ci-guard-fix.yml | 60 -------------------- 1 file changed, 60 deletions(-) delete mode 100644 .github/workflows/ied-relay-ci-guard-fix.yml diff --git a/.github/workflows/ied-relay-ci-guard-fix.yml b/.github/workflows/ied-relay-ci-guard-fix.yml deleted file mode 100644 index 0b79cfa1d..000000000 --- a/.github/workflows/ied-relay-ci-guard-fix.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Align IED relay build invariant - -on: - pull_request: - branches: - - main - types: [opened, synchronize, reopened] - -permissions: - contents: write - -jobs: - patch-build-guard: - if: github.event.pull_request.head.ref == 'agent/ied-relay-icon-industrial-qa' && github.actor != 'github-actions[bot]' - runs-on: windows-latest - steps: - - name: Checkout branch - uses: actions/checkout@v4 - with: - ref: agent/ied-relay-icon-industrial-qa - fetch-depth: 0 - - - name: Update relay-icon invariant - shell: pwsh - run: | - $path = '.github/workflows/build.yml' - $text = Get-Content $path -Raw - $old = @' - $main -notmatch '' -or -'@ - if (-not $text.Contains($old)) { - throw 'The legacy 44x44 relay invariant was not found.' - } - $text = $text.Replace($old, $new) - Set-Content -Path $path -Value $text -Encoding utf8 - - - name: Verify updated guard against current source - shell: pwsh - run: | - $main = Get-Content MainWindow.xaml -Raw - if ($main -notmatch '') { - throw 'The new industrial relay layout does not satisfy the updated invariant.' - } - - - name: Commit guard update - shell: pwsh - run: | - Remove-Item '.github/workflows/ied-relay-ci-guard-fix.yml' - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add .github/workflows/build.yml .github/workflows/ied-relay-ci-guard-fix.yml - git commit -m 'ci: align relay icon visual invariant' - git push origin HEAD:agent/ied-relay-icon-industrial-qa