chore: add one-shot IED relay patch script #2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 = ''' <!-- Static protection-relay fascia with a dedicated status label below it. --> | ||
| <StackPanel Grid.Row="0" Grid.RowSpan="2" Grid.Column="0" Width="60" | ||
| Margin="-2,0,-2,0" VerticalAlignment="Center" | ||
| HorizontalAlignment="Center" Panel.ZIndex="2"> | ||
| <Viewbox Width="50" Height="50" Stretch="Uniform" | ||
| HorizontalAlignment="Center"> | ||
| <Control x:Name="RelayDeviceIcon" Width="50" Height="50" | ||
| Foreground="#EF4444" | ||
| Template="{StaticResource IedRelayFrontPanelTemplate}"/> | ||
| </Viewbox> | ||
| <Border x:Name="MonitorStateBadge" | ||
| HorizontalAlignment="Center" Margin="0,2,0,0" | ||
| Background="#F5FFF0F1" BorderBrush="#F3A3A8" BorderThickness="1" | ||
| CornerRadius="3" Padding="4,1"> | ||
| <StackPanel Orientation="Horizontal"> | ||
| <Ellipse x:Name="MonitorStateDot" Width="3.8" Height="3.8" Fill="#E5484D" | ||
| Margin="0,0,2.5,0" VerticalAlignment="Center"/> | ||
| <TextBlock x:Name="MonitorStateText" Text="STOP" FontSize="6.8" | ||
| FontWeight="SemiBold" Foreground="#C92A32"/> | ||
| </StackPanel> | ||
| </Border> | ||
| </StackPanel> | ||
| ''' | ||
| replace_region( | ||
| "MainWindow.xaml", | ||
| "<!-- Compact feeder-protection relay fascia; state remains readable without covering the LCD. -->", | ||
| '<StackPanel Grid.Row="0" Grid.Column="1"', | ||
| main_block, | ||
| ) | ||
| io_block = ''' <StackPanel Grid.Column="0" Width="56" | ||
| HorizontalAlignment="Center" VerticalAlignment="Center"> | ||
| <Viewbox Width="46" Height="46" HorizontalAlignment="Center"> | ||
| <Control x:Name="RelayIcon" Width="50" Height="50" Foreground="#D94B55" | ||
| Template="{StaticResource IedRelayFrontPanelTemplate}"/> | ||
| </Viewbox> | ||
| <Border x:Name="StateBadge" HorizontalAlignment="Center" Margin="0,2,0,0" | ||
| Background="#FFF1F2" BorderBrush="#F0B7BC" | ||
| BorderThickness="1" CornerRadius="3" Padding="4,1"> | ||
| <Border.Visibility> | ||
| <MultiBinding Converter="{StaticResource AllPassedVisibilityConverter}" ConverterParameter="Inverse"> | ||
| <Binding Path="TestPoints.Count"/> | ||
| <Binding Path="PassedCount"/> | ||
| </MultiBinding> | ||
| </Border.Visibility> | ||
| <TextBlock x:Name="StateText" Text="{Binding CardStateText}" | ||
| FontSize="7" FontWeight="SemiBold" Foreground="#C53A45"/> | ||
| </Border> | ||
| </StackPanel> | ||
| <Grid Grid.Column="1" Margin="3,0,0,0"> | ||
| <Grid.RowDefinitions> | ||
| <RowDefinition Height="Auto"/> | ||
| <RowDefinition Height="Auto"/> | ||
| <RowDefinition Height="Auto"/> | ||
| <RowDefinition Height="Auto"/> | ||
| </Grid.RowDefinitions> | ||
| <Grid> | ||
| <Grid.ColumnDefinitions> | ||
| <ColumnDefinition Width="*"/> | ||
| <ColumnDefinition Width="Auto"/> | ||
| </Grid.ColumnDefinitions> | ||
| <TextBlock Text="{Binding IedName}" FontSize="12.8" FontWeight="SemiBold" | ||
| Foreground="{StaticResource Ink}" TextTrimming="CharacterEllipsis" | ||
| Margin="0,0,10,0" VerticalAlignment="Center"/> | ||
| <Border Grid.Column="1" Background="#EAF8F1" BorderBrush="#8FD1B1" | ||
| BorderThickness="1" CornerRadius="9" Padding="7,2" | ||
| Margin="10,0,0,0" VerticalAlignment="Center"> | ||
| <Border.Visibility> | ||
| <MultiBinding Converter="{StaticResource AllPassedVisibilityConverter}"> | ||
| <Binding Path="TestPoints.Count"/> | ||
| <Binding Path="PassedCount"/> | ||
| </MultiBinding> | ||
| </Border.Visibility> | ||
| <TextBlock Text="✔ PASS" FontSize="8.5" FontWeight="Bold" Foreground="#16845A"/> | ||
| </Border> | ||
| </Grid> | ||
| ''' | ||
| replace_region( | ||
| "IoListTestingWindow.xaml", | ||
| '<Viewbox Width="50" Height="50" HorizontalAlignment="Center" VerticalAlignment="Center">', | ||
| '<TextBlock Grid.Row="1" Text="{Binding IpAddress}"', | ||
| io_block, | ||
| ) | ||
| tests = Path("tests/ARSAS.Tests/IoTestingUiContractTests.cs") | ||
| test_text = tests.read_text(encoding="utf-8") | ||
| test_start = test_text.index(' [Fact]\n public void IedCards_UseReusableNumericalRelayFrontPanelInsteadOfCalculatorKeypad()') | ||
| test_end = test_text.index(' [Fact]\n public void IoTestingWindow_UsesBalancedInitialGridWidthsAndCenteredRelayHeaders()', test_start) | ||
| test_method = ''' [Fact] | ||
| public void IedCards_UseReusableNumericalRelayFrontPanelInsteadOfCalculatorKeypad() | ||
| { | ||
| XNamespace presentation = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"; | ||
| XNamespace x = "http://schemas.microsoft.com/winfx/2006/xaml"; | ||
| var resources = XDocument.Load(FindRepoFile("App.xaml")); | ||
| var template = resources | ||
| .Descendants(presentation + "ControlTemplate") | ||
| .Single(node => (string?)node.Attribute(x + "Key") == "IedRelayFrontPanelTemplate"); | ||
| var namedParts = template | ||
| .Descendants() | ||
| .Select(node => (string?)node.Attribute(x + "Name")) | ||
| .Where(name => !string.IsNullOrWhiteSpace(name)) | ||
| .Cast<string>() | ||
| .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 | ||