Redraw the host keys screen with its pins' own facts beside it

This commit is contained in:
2026-08-08 14:17:19 +02:00
parent c8507b44fe
commit bb2f973687
@@ -15,34 +15,78 @@
The data layer did not move and did not change. Every pin is still a vault item, still end-to-end The data layer did not move and did not change. Every pin is still a vault item, still end-to-end
encrypted, still synced; see KnownHostSecret. What is here is a screen over VaultViewModel.KnownHostPins. encrypted, still synced; see KnownHostSecret. What is here is a screen over VaultViewModel.KnownHostPins.
v5c-3: restyled against KnownHosts.dc.html — still a main-chrome screen (the rail's own Keys item stays
the way in), a 40px back arrow to Keychain rather than the old link inside it, a bordered radius-12
container in place of the plain list-and-sidebar split, and a Copy fingerprint button beside Withdraw
pin. Everything the screen could do before still can: Filter, Selected, ForgetSelectedCommand and the
provenance notes below are unchanged, just repainted.
--> -->
<Grid ColumnDefinitions="*,244"> <UserControl.Styles>
<!--
A row, not a card: Track fill on hover, Track fill plus an accent ring on the selected one, matching
the design's own "selected = Track bg + accent ring, hover Track" — the same idiom SnippetsScreen's
own ListBox.snipcards uses, at this design's own 8px radius rather than that one's 10.
-->
<Style Selector="ListBox.pinrows > ListBoxItem /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="CornerRadius" Value="8" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
<Style Selector="ListBox.pinrows > ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource Track}" />
</Style>
<Style Selector="ListBox.pinrows > ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource Track}" />
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
</Style>
<Style Selector="ListBox.pinrows > ListBoxItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource Track}" />
<Setter Property="BorderBrush" Value="{StaticResource Accent}" />
</Style>
</UserControl.Styles>
<Grid Grid.Column="0" RowDefinitions="Auto,Auto,*"> <Grid RowDefinitions="Auto,Auto,*" Margin="26">
<Border Grid.Row="0" Padding="14,0" Height="44" <!-- ============ HEADER ============ -->
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1"> <Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,Auto,*,Auto">
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center"> <Button Grid.Column="0" Classes="ghost" Width="40" Height="40" Padding="0"
<TextBlock Grid.Column="0" Classes="mono" Text="HOST KEYS" FontSize="12" Command="{Binding BackCommand}" ToolTip.Tip="Back to Keychain">
FontWeight="SemiBold" LetterSpacing="1" Foreground="{StaticResource Text}" <TextBlock FontFamily="{StaticResource IconFont}" Text="&#xE5C4;" FontSize="17"
Foreground="{StaticResource TextFaint}" HorizontalAlignment="Center"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Summary}" FontSize="10.5" </Button>
Foreground="{StaticResource TextFaint}" Margin="10,0,0,0" <TextBlock Grid.Column="1" Text="Host keys" FontSize="33" FontWeight="Bold" LetterSpacing="-0.5"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" /> Foreground="{StaticResource Text}" VerticalAlignment="Center" Margin="14,0,0,0" />
<Border Grid.Column="2" MinWidth="30" Height="26" CornerRadius="8" Padding="8,0" Margin="12,0,0,0"
Background="{StaticResource Chip}" VerticalAlignment="Center" IsVisible="{Binding HasPins}">
<TextBlock Classes="mono" Text="{Binding Count}" FontSize="12.5" FontWeight="SemiBold"
Foreground="{StaticResource TextDim}" HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<!-- <!--
Matches fingerprints as well as host names, which is the point of it. What somebody does with Matches fingerprints as well as host names, which is the point of it. What somebody does with
this screen is check whether a published SHA256:… is the one they approved, and searching only this screen is check whether a published SHA256:… is the one they approved, and searching only
by name would answer a different question. by name would answer a different question.
--> -->
<TextBox Grid.Column="2" x:Name="PinFilter" Text="{Binding Filter}" Width="240" <TextBox Grid.Column="4" x:Name="PinFilter" Text="{Binding Filter}" Width="320" Height="40"
PlaceholderText="filter by host or fingerprint" VerticalAlignment="Center" /> PlaceholderText="filter by host or fingerprint" VerticalAlignment="Center" />
</Grid> </Grid>
</Border>
<Grid Grid.Row="1" ColumnDefinitions="2,1.4*,58,104,*,96" Margin="0,6,14,6" <TextBlock Grid.Row="1" Classes="hint" FontSize="12" Margin="0,10,0,0" TextWrapping="Wrap"
IsVisible="{Binding HasVisiblePins}"> Text="Every pin is a decision recorded at the moment of connecting. Fingerprints are never trimmed — compare them character by character against what the operator published." />
<!-- ============ THE TABLE ============ -->
<Border Grid.Row="2" Margin="0,18,0,0" CornerRadius="12" BorderBrush="{StaticResource Border}"
BorderThickness="1" ClipToBounds="True">
<Grid ColumnDefinitions="*,320">
<Grid Grid.Column="0" Background="{StaticResource Pane}" RowDefinitions="Auto,*">
<Border Grid.Row="0" Padding="24,14,24,10" BorderBrush="{StaticResource Border}"
BorderThickness="0,0,0,1" IsVisible="{Binding HasVisiblePins}">
<Grid ColumnDefinitions="2,1.4*,58,104,*,96">
<TextBlock Grid.Column="1" Classes="label" Text="HOST" FontSize="9.5" LetterSpacing="1" <TextBlock Grid.Column="1" Classes="label" Text="HOST" FontSize="9.5" LetterSpacing="1"
Margin="12,0,8,0" /> Margin="12,0,8,0" />
<TextBlock Grid.Column="2" Classes="label" Text="PORT" FontSize="9.5" LetterSpacing="1" /> <TextBlock Grid.Column="2" Classes="label" Text="PORT" FontSize="9.5" LetterSpacing="1" />
@@ -50,89 +94,124 @@
<TextBlock Grid.Column="4" Classes="label" Text="FINGERPRINT" FontSize="9.5" LetterSpacing="1" /> <TextBlock Grid.Column="4" Classes="label" Text="FINGERPRINT" FontSize="9.5" LetterSpacing="1" />
<TextBlock Grid.Column="5" Classes="label" Text="APPROVED" FontSize="9.5" LetterSpacing="1" /> <TextBlock Grid.Column="5" Classes="label" Text="APPROVED" FontSize="9.5" LetterSpacing="1" />
</Grid> </Grid>
</Border>
<ListBox Grid.Row="2" x:Name="PinList" Focusable="True" <ListBox Grid.Row="1" x:Name="PinList" Classes="pinrows" Focusable="True" Margin="12,8"
ItemsSource="{Binding VisiblePins}" ItemsSource="{Binding VisiblePins}" SelectedItem="{Binding Selected}">
SelectedItem="{Binding Selected}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:KnownHostRowViewModel"> <DataTemplate x:DataType="vm:KnownHostRowViewModel">
<Grid ColumnDefinitions="2,1.4*,58,104,*,96" Margin="0,7,14,7"> <Grid ColumnDefinitions="2,1.4*,58,104,*,96" Margin="12">
<Border Grid.Column="0" Classes="rowmark" /> <Border Grid.Column="0" Classes="rowmark" />
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Host}" FontSize="12" <TextBlock Grid.Column="1" Classes="mono" Text="{Binding Host}" FontSize="12.5"
FontWeight="Medium" Foreground="{StaticResource Text}" Margin="12,0,8,0" FontWeight="Medium" Foreground="{StaticResource Text}" Margin="12,0,8,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" /> TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Port}" FontSize="10.5" <TextBlock Grid.Column="2" Classes="mono" Text="{Binding Port}" FontSize="11"
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" /> Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Algorithm}" FontSize="10" <TextBlock Grid.Column="3" Classes="mono" Text="{Binding Algorithm}" FontSize="10.5"
Foreground="{StaticResource TextDim}" Margin="0,0,8,0" Foreground="{StaticResource TextDim}" Margin="0,0,8,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" /> TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<!-- <!--
Never trimmed, and this column is why the table is laid out the way it is. The only thing Never trimmed, and this column is why the table is laid out the way it is. The only thing
anybody does with a fingerprint is compare it character by character against one an operator anybody does with a fingerprint is compare it character by character against one an
published; an ellipsis in the middle turns that into a glance, which is the habit the whole operator published; an ellipsis in the middle turns that into a glance, which is the habit
mechanism exists to replace. the whole mechanism exists to replace.
--> -->
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding Fingerprint}" FontSize="10.5" <TextBlock Grid.Column="4" Classes="mono" Text="{Binding Fingerprint}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" Margin="0,0,8,0" Foreground="{StaticResource TextFaint}" Margin="0,0,8,0"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
<TextBlock Grid.Column="5" Classes="mono" Text="{Binding Approved}" FontSize="10" <TextBlock Grid.Column="5" Classes="mono" Text="{Binding Approved}" FontSize="10.5"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" /> Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
<TextBlock Grid.Row="2" Classes="hint" Text="{Binding EmptyMessage}" FontSize="12" <TextBlock Grid.Row="1" Classes="hint" Text="{Binding EmptyMessage}" FontSize="12"
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
TextAlignment="Center" MaxWidth="340" TextAlignment="Center" MaxWidth="340" IsVisible="{Binding !HasVisiblePins}" />
IsVisible="{Binding !HasVisiblePins}" />
</Grid> </Grid>
<!-- ============ DETAIL SIDEBAR ============ -->
<Border Grid.Column="1" Background="{StaticResource Sidebar}" <Border Grid.Column="1" Background="{StaticResource Sidebar}"
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0"> BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0">
<ScrollViewer> <ScrollViewer>
<StackPanel Margin="14,16" Spacing="6"> <StackPanel Margin="20,22" Spacing="14">
<TextBlock Classes="hint" FontSize="12" <TextBlock Classes="hint" FontSize="12"
Text="Choose a pinned key to see it in full, and to withdraw it." Text="Choose a pinned key to see it in full, and to withdraw it."
IsVisible="{Binding !HasSelection}" /> IsVisible="{Binding !HasSelection}" />
<StackPanel Spacing="6" IsVisible="{Binding HasSelection}"> <StackPanel Spacing="14" IsVisible="{Binding HasSelection}">
<TextBlock Classes="mono" Text="{Binding Selected.Label}" FontSize="13"
FontWeight="SemiBold" Foreground="{StaticResource Text}" TextWrapping="Wrap" />
<Border Classes="chip warn" HorizontalAlignment="Left" <StackPanel Orientation="Horizontal" Spacing="12">
<Border Width="36" Height="36" CornerRadius="10" Background="{StaticResource AccentSoft}">
<TextBlock FontFamily="{StaticResource IconFont}" Text="&#xE90D;" FontSize="17"
Foreground="{StaticResource AccentText}" HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<StackPanel Spacing="4" VerticalAlignment="Center">
<TextBlock Classes="mono" Text="{Binding Selected.Host}" FontSize="13.5" FontWeight="Bold"
Foreground="{StaticResource Text}" TextWrapping="Wrap" />
<TextBlock Classes="mono" Text="{Binding Selected.Label}" FontSize="10.5"
Foreground="{StaticResource TextDim}" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<Border Classes="chip warn" Background="{StaticResource WarnWash}" HorizontalAlignment="Left"
IsVisible="{Binding !Selected.IsDialledByAHost}"> IsVisible="{Binding !Selected.IsDialledByAHost}">
<TextBlock Text="no host uses this" /> <TextBlock Text="no host uses this" />
</Border> </Border>
<TextBlock Classes="label" Text="FINGERPRINT" Margin="0,12,0,4" /> <StackPanel Spacing="6">
<Border Background="{StaticResource Raised}" BorderBrush="{StaticResource Border}" <TextBlock Classes="label" Text="FINGERPRINT" />
BorderThickness="1" CornerRadius="4" Padding="8"> <Border Background="{StaticResource Pane}" BorderBrush="{StaticResource Border}"
BorderThickness="1" CornerRadius="10" Padding="12,10">
<SelectableTextBlock Classes="mono" Text="{Binding Selected.Fingerprint}" <SelectableTextBlock Classes="mono" Text="{Binding Selected.Fingerprint}"
FontSize="10.5" Foreground="{StaticResource TextDim}" FontSize="10.5" Foreground="{StaticResource TextDim}"
TextWrapping="Wrap" /> TextWrapping="Wrap" LineHeight="16" />
</Border> </Border>
</StackPanel>
<TextBlock Classes="label" Text="APPROVED" Margin="0,12,0,4" /> <StackPanel Spacing="6">
<TextBlock Classes="mono" Text="{Binding Selected.Approved}" FontSize="11" <TextBlock Classes="label" Text="APPROVED" />
Foreground="{StaticResource TextDim}" /> <TextBlock Classes="mono" Text="{Binding Selected.Approved}" FontSize="11.5"
Foreground="{StaticResource TextDim}" TextWrapping="Wrap" />
<!-- <!--
Said rather than implied. No vault item carries a timestamp, so this date is read back out of Said rather than implied. No vault item carries a timestamp, so this date is read back out
the item's own version 7 id — which records when the pin was created and knows nothing about of the item's own version 7 id — which records when the pin was created and knows nothing
it being re-approved since. Presenting that as "last used" would be inventing a fact. about it being re-approved since. Presenting that as "last used" would be inventing a fact.
--> -->
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap" <TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
Text="Taken from the item's identifier, so it is when this key was first approved — not when it was last checked. Nothing here records that." /> Text="Taken from the item's identifier, so it is when this key was first approved — not when it was last checked. Nothing here records that." />
</StackPanel>
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap" Margin="0,12,0,0" <!--
◆ VAULT. Real, not decorative — KnownHostRowViewModel.VaultName comes off the same
VaultId/VaultName pair HostRowViewModel carries, so this chip never names a vault the pin
is not actually stored in.
-->
<StackPanel Spacing="6">
<TextBlock Classes="label" Text="VAULT" />
<Border Classes="chip" HorizontalAlignment="Left">
<TextBlock Text="{Binding Selected.VaultName}" />
</Border>
</StackPanel>
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap" Margin="0,4,0,0"
Text="A pin outlives whatever it was approved for: deleting a host leaves it, and so does changing a host's address. That is deliberate — trust is about the endpoint, not the bookmark." /> Text="A pin outlives whatever it was approved for: deleting a host leaves it, and so does changing a host's address. That is deliberate — trust is about the endpoint, not the bookmark." />
<Button Classes="danger" Content="FORGET THIS HOST KEY" Margin="0,12,0,0" <StackPanel Spacing="8" Margin="0,10,0,0">
HorizontalAlignment="Left" <Button Classes="ghost" Height="38" HorizontalAlignment="Stretch"
Command="{Binding ForgetSelectedCommand}" Content="COPY FINGERPRINT" Command="{Binding CopyFingerprintCommand}"
ToolTip.Tip="Fingerprints are public. Puts it on the clipboard in full." />
<Button Classes="danger" Height="38" HorizontalAlignment="Stretch"
Content="WITHDRAW PIN" Command="{Binding ForgetSelectedCommand}"
ToolTip.Tip="Withdraws trust. The next connection to this endpoint asks you to check the fingerprint again, which is the safe direction to be wrong in — and it is the way back from a server that was legitimately rebuilt." /> ToolTip.Tip="Withdraws trust. The next connection to this endpoint asks you to check the fingerprint again, which is the safe direction to be wrong in — and it is the way back from a server that was legitimately rebuilt." />
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap" TextAlignment="Center"
Text="The next connection will ask you to approve this host's key again." />
</StackPanel>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
@@ -140,5 +219,8 @@
</Border> </Border>
</Grid> </Grid>
</Border>
</Grid>
</UserControl> </UserControl>