Files
DodoSSH/src/DodoSSH.Client.App/Views/PreferencesScreen.axaml
T
jaap-jan fe9d7fc289 Give DodoSSH a phone, and a shared shell for both heads to drive
The Android head from docs/android-port.md, taken as far as its step 6.

Step 3, the spike, is answered and its throwaway screen is gone: libsodium.so and
libe_sqlite3.so are both in the arm64 APK, so NSec resolves its native half on Android
despite shipping no Android build, and the local cache opens. Two findings the audit
could not have had: Avalonia.Controls.WebView only ships net10.0-android36.0, which
settles the open "which Android versions" question at targetSdk 36; and Android has
blocked cleartext HTTP since API 28, so the terminal renderer needs a network security
config scoped to 127.0.0.1 or the WebView loads nothing.

DodoSSH.Client.Shell is new and is why the phone can exist: the view models, the terminal
renderer files and the palette moved there so both heads drive one state machine and draw
from one set of tokens. The desktop head is otherwise untouched and its 144 tests still
pass.

The platform pieces behind interfaces that already existed: the profile directory from
filesDir, a device key wrapped by a StrongBox-backed key that a fingerprint releases, and
a foreground service so a shell outliving a vault lock stays true on a platform that
stops backgrounded processes.

Sign-in is deliberately absent rather than approximated. It needs an app link, because
reusing the desktop loopback listener is the attack RFC 8252 section 8.3 names.
2026-07-31 20:58:48 +02:00

155 lines
9.4 KiB
XML

<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
xmlns:views="using:DodoSSH.Client.App.Views"
x:Class="DodoSSH.Client.App.Views.PreferencesScreen"
x:DataType="vm:MainWindowViewModel">
<!--
Preferences.
The design's rail has six sections and its TERMINAL panel has six settings. None of the six settings can
be saved: there is no preferences table in the local cache, no preference item type in the vault, and no
channel from the host to the renderer that carries a terminal option — the data plane's frames are
output, session-opened, session-closed and session-activated, and nothing else. A font-size stepper
that reset on every launch would be worse than not having one.
So this screen ships what is real, which is not nothing: this machine's device key is a genuine
preference with a genuine effect, and it is the one thing on the design's SECURITY panel that exists.
The two commands behind it were already in the shell; they were merely homeless, wedged into the old
account bar because there was nowhere else to put them.
Everything else is listed as absent rather than omitted, because a preferences screen that is silent
about the settings it has not got reads as a product with six preferences.
-->
<ScrollViewer>
<StackPanel MaxWidth="620" Margin="28,26" HorizontalAlignment="Left">
<TextBlock Classes="mono" Text="THIS MACHINE" FontSize="13" FontWeight="SemiBold"
LetterSpacing="1" Foreground="{StaticResource Text}" />
<Grid ColumnDefinitions="*,Auto" Margin="0,12,0,0">
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="Unlock with Windows Hello" Foreground="{StaticResource Text}" FontSize="12"
FontWeight="Medium" />
<TextBlock Classes="hint" FontSize="10"
Text="Registers this machine so a later launch can open the vault with a Windows confirmation instead of your passphrase. Your passphrase keeps working." />
</StackPanel>
<Button Grid.Column="1" Classes="accent" Content="REGISTER"
Command="{Binding RegisterDeviceCommand}"
IsEnabled="{Binding !IsBusy}"
IsVisible="{Binding CanRegisterDevice}" />
<!--
The withdrawal, in the place the offer was. Its own flag rather than the negation of that one: a
machine with no TPM and a machine that is already registered are both "cannot register", and only
the second has anything to take back.
-->
<Button Grid.Column="1" Classes="danger" Content="STOP UNLOCKING HERE"
Command="{Binding ForgetDeviceCommand}"
IsEnabled="{Binding !IsBusy}"
IsVisible="{Binding CanForgetDevice}"
ToolTip.Tip="Withdraws this machine's device key, here and from your account, so it goes back to asking for your passphrase. Do this to a machine you have lost." />
</Grid>
<!-- Neither flag is set on a machine that cannot keep a key at all, and that is worth saying. -->
<TextBlock Classes="hint" FontSize="10" Margin="0,8,0,0"
Text="This machine has nowhere to keep a device key, so the vault will keep asking for your passphrase. That needs a TPM and a Windows keystore willing to release the key."
IsVisible="{Binding HasNoDeviceKeyOption}" />
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
<TextBlock Classes="mono" Text="VAULT" FontSize="13" FontWeight="SemiBold"
LetterSpacing="1" Foreground="{StaticResource Text}" />
<Grid ColumnDefinitions="*,Auto" Margin="0,12,0,0">
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="Lock the vault" Foreground="{StaticResource Text}" FontSize="12"
FontWeight="Medium" />
<TextBlock Classes="hint" FontSize="10"
Text="Closes the vault and forgets every key it held. Shells you have open keep running and reappear when you unlock — locked describes the vault, not this machine's access to your hosts." />
</StackPanel>
<Button Grid.Column="1" Classes="ghost" Content="LOCK NOW" Command="{Binding LockCommand}" />
</Grid>
<Grid ColumnDefinitions="*,Auto" Margin="0,14,0,0">
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="Synchronise" Foreground="{StaticResource Text}" FontSize="12"
FontWeight="Medium" />
<TextBlock Classes="hint" FontSize="10"
Text="Runs a pass now. One runs on its own when the vault opens, straight after any change, and every minute while it stays open — and a pass that finds this machine offline signs it back in from the session it remembered, so nothing here depends on being pressed." />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6">
<Button Classes="ghost" Content="SIGN IN" Command="{Binding SignInCommand}"
IsVisible="{Binding !IsOnline}"
ToolTip.Tip="Opens your browser. Only needed when there is no remembered session to resume — after signing out, or once your identity provider stops accepting the one this machine held." />
<Button Classes="ghost" Content="SYNC NOW" Command="{Binding Vault.SyncCommand}" />
</StackPanel>
</Grid>
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
<TextBlock Classes="mono" Text="ACCOUNT" FontSize="13" FontWeight="SemiBold"
LetterSpacing="1" Foreground="{StaticResource Text}" />
<TextBlock Classes="mono" Text="{Binding AccountName}" FontSize="11" Margin="0,8,0,0"
Foreground="{StaticResource Info}" TextTrimming="CharacterEllipsis" />
<Grid ColumnDefinitions="*,Auto" Margin="0,12,0,0">
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="Sign out of this machine" Foreground="{StaticResource Text}" FontSize="12"
FontWeight="Medium" />
<TextBlock Classes="hint" FontSize="10"
Text="Deletes this machine's copy of the vault and withdraws its device key, so it goes back to knowing nothing. The vault stays on the server; signing in again brings it back. Use this to hand a machine on, or to enrol a different account." />
</StackPanel>
<!--
Hidden rather than disabled while the confirmation is up, because the card below carries the
button that actually does it and two sign-out buttons on one screen is one too many.
-->
<Button Grid.Column="1" Classes="danger" Content="SIGN OUT"
Command="{Binding SignOutCommand}"
IsEnabled="{Binding !IsBusy}"
IsVisible="{Binding !IsConfirmingSignOut}" />
</Grid>
<Border Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}"
BorderThickness="1" CornerRadius="6" Padding="14" Margin="0,14,0,0"
IsVisible="{Binding IsConfirmingSignOut}">
<views:SignOutCard />
</Border>
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
<TextBlock Classes="mono" Text="NOT BUILT YET" FontSize="13" FontWeight="SemiBold"
LetterSpacing="1" Foreground="{StaticResource TextDim}" />
<TextBlock Classes="hint" FontSize="11" Margin="0,8,0,0"
Text="These are on the design and have nothing behind them. They are listed rather than left out, so that what this screen does not do is as legible as what it does. The full list, and what each would take, is in docs/design-import-gaps.md." />
<ItemsControl Margin="0,12,0,0">
<ItemsControl.Styles>
<Style Selector="TextBlock.gap">
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="Margin" Value="0,0,0,7" />
</Style>
</ItemsControl.Styles>
<TextBlock Classes="gap"
Text="Terminal font, size, cursor and scrollback — the renderer hard-codes them, and nothing carries a change to it." />
<TextBlock Classes="gap"
Text="Any preference at all, saved — there is no preferences store in the local cache and no preference item type in the vault." />
<TextBlock Classes="gap"
Text="Auto-lock after idle — nothing tracks idleness, and the lock policy would have to decide what to do about a shell mid-job." />
<TextBlock Classes="gap"
Text="Per-use approval before a key signs — keys are handed to the SSH stack whole at connect time, so there is no per-signature moment to interrupt." />
<TextBlock Classes="gap"
Text="SSO and team policy — the server has no team endpoints, so there is no policy for this screen to show." />
<TextBlock Classes="gap"
Text="Keyboard shortcuts — the window binds one chord, and the terminal keeps the rest for the remote." />
</ItemsControl>
</StackPanel>
</ScrollViewer>
</UserControl>