Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ jobs:
$main -notmatch 'Header="Flags"' -or
$main -notmatch 'Header="Diagnostics" Binding="\{Binding DiagnosticsSummary\}"' -or
$main -notmatch '<ColumnDefinition Width="56"/>' -or
$main -notmatch '<Viewbox Width="44" Height="44" Stretch="Uniform"' -or
$main -notmatch '<Viewbox Width="50" Height="50" Stretch="Uniform"' -or
$main -notmatch 'x:Name="MonitorStateBadge"' -or
$main -match '<Control\.Effect><DropShadowEffect BlurRadius="5" ShadowDepth="1" Opacity="0\.22" Color="#26313B"/></Control\.Effect>' -or
$main -notmatch 'LucideSquare.*Stroke="\{StaticResource Warning\}"' -or
$main -notmatch 'LucideX.*Stroke="\{StaticResource Danger\}"' -or
$app -notmatch 'x:Key="CommandOpenGradient"' -or
Expand Down
57 changes: 30 additions & 27 deletions IoListTestingWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,25 @@
<ColumnDefinition Width="58"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Viewbox Width="50" Height="50" HorizontalAlignment="Center" VerticalAlignment="Center">
<Control x:Name="RelayIcon" Width="50" Height="50" Foreground="#D94B55"
Template="{StaticResource IedRelayFrontPanelTemplate}"/>
</Viewbox>
<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"/>
Expand All @@ -185,29 +200,17 @@
<TextBlock Text="{Binding IedName}" FontSize="12.8" FontWeight="SemiBold"
Foreground="{StaticResource Ink}" TextTrimming="CharacterEllipsis"
Margin="0,0,10,0" VerticalAlignment="Center"/>
<Grid Grid.Column="1" Margin="10,0,0,0" VerticalAlignment="Center">
<Border x:Name="StateBadge" Background="#FFF1F2"
BorderBrush="#F0B7BC" BorderThickness="1" CornerRadius="9" Padding="7,2">
<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="8.5" FontWeight="Bold" Foreground="#C53A45"/>
</Border>
<Border Background="#EAF8F1" BorderBrush="#8FD1B1"
BorderThickness="1" CornerRadius="9" Padding="7,2">
<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>
<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>
<TextBlock Grid.Row="1" Text="{Binding IpAddress}" Style="{StaticResource Caption}"
Margin="0,3,0,0"/>
Expand Down
43 changes: 22 additions & 21 deletions MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -229,27 +229,28 @@
<ColumnDefinition Width="56"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Compact feeder-protection relay fascia; state remains readable without covering the LCD. -->
<Grid Grid.Row="0" Grid.RowSpan="2" Grid.Column="0" Width="60" Height="60" Margin="-2,0,-2,0"
VerticalAlignment="Center" ClipToBounds="False" Panel.ZIndex="2">
<Viewbox Width="54" Height="54" Stretch="Uniform"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Control x:Name="RelayDeviceIcon" Width="50" Height="50"
Foreground="#EF4444"
Template="{StaticResource IedRelayFrontPanelTemplate}">
<Control.Effect><DropShadowEffect BlurRadius="5" ShadowDepth="1" Opacity="0.22" Color="#26313B"/></Control.Effect>
</Control>
</Viewbox>
<Border x:Name="MonitorStateBadge"
HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="7,0,0,7"
Background="#F5FFF0F1" BorderBrush="#F3A3A8" BorderThickness="1"
CornerRadius="2.8" Padding="3.5,0.7" Panel.ZIndex="5">
<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>
</Grid>
<!-- 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>

<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Top" HorizontalAlignment="Stretch" Margin="4,0,12,0">
<TextBlock Text="{Binding Name}" FontSize="12.8" FontWeight="SemiBold"
Expand Down
57 changes: 26 additions & 31 deletions design-qa.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,40 @@
# IED protection relay fascia design QA

- Source visual truth: `C:\Users\me\AppData\Local\Temp\codex-clipboard-2ff37ea3-1b1d-4eb5-a340-121885a3c2f2.png`
- Implementation asset: `D:\Git\arsas\Assets\ied-protection-relay-fascia.png`
- WPF consumer: `IedRelayFrontPanelTemplate` in `App.xaml`, rendered at 50 x 50 device-independent pixels
- Source pixels: 400 x 400
- Implementation pixels: 1250 x 1250, normalized to 400 x 400 for comparison
- Density check: implementation was also downsampled to 50 x 50 and enlarged with nearest-neighbor only for legibility inspection
- State: static fascia artwork; LIVE/STOP and connection state remain data-driven WPF overlays
## Scope

## Full-view comparison evidence
- 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

Comparison image: `C:\Users\me\.codex\visualizations\2026\08\03\019fc7bf-2e84-7492-be9f-2cb90454829e\arsas-relay-reference-vs-fascia-asset.png`
## Implementation checks

The implementation preserves the reference's defining protection-relay proportions: nearly square panel-mount bezel, warm-gray fascia, wide dark display surround, central pale LCD, left and right LED banks, cyan directional keypad, and a separate vertical function-key bank. Brand and model marks were intentionally omitted.
- 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.

## Focused-region comparison evidence
## Validation

Small-scale image: `C:\Users\me\.codex\visualizations\2026\08\03\019fc7bf-2e84-7492-be9f-2cb90454829e\arsas-relay-fascia-at-50px.png`
Verified on GitHub Actions using Windows Server 2025 and .NET SDK 8.0.423:

At the production 50 x 50 size, micro-copy is no longer readable, as expected, but the dark square bezel, LCD, LED columns, and cyan keypad remain distinct. The icon reads as a feeder protection relay rather than a calculator.
```powershell
dotnet build ArIED61850Tester.csproj -c Release
dotnet test tests/ARSAS.Tests/ARSAS.Tests.csproj -c Release --filter "FullyQualifiedName~IoTestingUiContractTests"
```

## Required fidelity surfaces
Results:

- Fonts and typography: no manufacturer or product typography was copied; micro-measurements remain screen texture at icon scale.
- Spacing and layout rhythm: the bezel, display block, and lower keypad retain the reference's upper-display/lower-controls hierarchy.
- Colors and visual tokens: charcoal, warm gray, pale LCD blue, restrained status LEDs, and cyan keys match the source character without adding glow.
- Image quality and asset fidelity: a dedicated raster asset is used, with high-quality WPF bitmap scaling; no placeholder or approximate keypad geometry remains in XAML.
- Copy and content: no brand name or model identifier is included; ARSAS LIVE/STOP copy remains outside the artwork and data-driven.
- 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`.

## Findings
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.

No actionable P0, P1, or P2 fidelity differences remain for the fascia component.
## Visual QA status

## Comparison history
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.

- Initial vector implementation was too tall and contained a dial and terminal strip, so it did not match the selected square feeder-relay reference.
- Replaced it with a dedicated square fascia asset, removed the colored device glow, moved the state badge away from the LCD, and rechecked the asset at 50 x 50.
- Post-fix evidence is recorded in the full-view and focused comparison images above.

## Follow-up polish

- P3: capture a full in-app screenshot when desktop preview execution is available again to confirm the exact LIVE/STOP overlay position against real card content. Build and UI contract verification already pass.

final result: passed
Final result: code structure, Release build, and 11/11 UI contracts passed; runtime screenshot evidence remains a follow-up polish item.
34 changes: 28 additions & 6 deletions tests/ARSAS.Tests/IoTestingUiContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,34 @@ public void IedCards_UseReusableNumericalRelayFrontPanelInsteadOfCalculatorKeypa
var project = File.ReadAllText(FindRepoFile("ArIED61850Tester.csproj"));
Assert.Contains("Assets\\ied-protection-relay-fascia.png", project, StringComparison.Ordinal);

var explorer = File.ReadAllText(FindRepoFile("MainWindow.xaml"));
var ioTesting = File.ReadAllText(FindRepoFile("IoListTestingWindow.xaml"));
Assert.Contains("IedRelayFrontPanelTemplate", explorer, StringComparison.Ordinal);
Assert.Contains("IedRelayFrontPanelTemplate", ioTesting, StringComparison.Ordinal);
Assert.DoesNotContain("M 2 0 L 2 20", explorer, StringComparison.Ordinal);
Assert.DoesNotContain("M 2 0 L 2 20", ioTesting, 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);
}

[Fact]
Expand Down
Loading