Public Access
An estate on Hetzner is already described somewhere: the project lists every server, its addresses, its plan and where it runs. Getting that into a keychain meant copying forty addresses out of a browser by hand, which is the same work the ssh_config importer exists to remove for the machines described in a file. ◆ A WIZARD RATHER THAN A SCREEN, AND THE DIFFERENCE IS WHAT EACH STEP COSTS. ImportScreen reads a file that is already on this machine, so it scans on open and shows a list. This cannot: it needs a credential that has to be made in another company's console first, and then it makes a request over the internet. Those are two moments rather than one, and a screen that mixed the token box in with the list would ask for the credential again every time somebody came back to look at the list. So three steps — token, choose, done — and the first two write nothing. Step two carries one address choice, one login user and one port for the whole import, because an estate is reached the same way throughout and a per-server dropdown would be forty answers to one question. ◆ THE TOKEN IS USED ONCE AND CLEARED, INCLUDING WHEN IT WORKED. The half worth stating is the success path. Clearing only on failure would leave a credential capable of listing somebody's whole estate sitting in a text box for as long as they spend reading forty rows, which is the longest anybody stands on this screen. Nothing writes it to the vault, to settings or to a log, and reopening the wizard restarts at step one rather than resuming — a resumed step two would be a list nothing on screen has the means to refresh, going on looking current for as long as the application stays open. Read is the only permission it needs and the note above the box says so: a Read & Write token would work identically here and would be a credential capable of deleting the estate, pasted in for no gain. The note also says a token belongs to one project — there is no API call that will say which, and none that lists the others — so an account with four projects runs this four times. Without that sentence the wizard looks like it lost most of the estate. ◆ THE ADDRESS IS A CHOICE, AND TWO OF THE THREE FALL BACK WHERE THE THIRD MUST NOT. Public IPv4 and public IPv6 fall back to each other: the same machine over the same internet, so the worst case is a route nobody picked, and the row says which. Private does not fall back at all. A server on no private network is simply not importable under that choice — its tickbox is disabled, and neither direction of the header's tick-all can reach it — because falling back would put an address on the open internet into a bookmark that was asked to stay off it. IPv6 needs saying twice over. Hetzner routes a whole /64 and the API reports the network, which nothing can dial; the address a stock image answers on is the first one in it. That is a convention of Hetzner's own images rather than a fact the API stated, so every use of it carries a warning on the row and in the stored note, and it is computed by setting the low byte rather than by string surgery on the prefix. It is not an edge case either — IPv4 is billed separately now and a project built since then can be entirely IPv6-only. ◆ NOTHING IMPORTED HERE AUTHENTICATES, AND THAT IS SAID BEFORE THE BUTTON. Hetzner has never held the private half of an SSH key. The API will name a project's keys and hand over their public halves; there is no private half to bring. So every host arrives bound to nothing and asks for a password until somebody points it at a key already in the keychain — the same shape an ssh_config import has with its key tick off, and stated above the button on the same reasoning: somebody who is not told will conclude the import was broken. What the note keeps is what does not go stale — the id, the plan, the location, the image, the labels. The power state is a column and never a note: "running" about a machine somebody switched off last March is worse than no note. ◆ WHERE IT LIVES: TWO FLAGS OVER ONE CONTENT COLUMN. Drawn over the Preferences page inside settings mode exactly as ImportScreen is, with SettingsNav unmoved and the titlebar offering "Back to preferences". IsHetznerImportOpen is a second flag rather than a "which importer" enum, because they are two independent facts about the content column and a single value would need a "neither" member IsImportOpen already expresses; what keeps them from both being true is that every path that opens one closes the other. The titlebar and the Preferences page ask IsImporterOpen, which is written against both. Preferences grows a second importer row beside the first rather than one row that asks "from where?", which would put a question in front of the far more common answer. ◆ WHAT IS NOT HERE. Hetzner's Robot — dedicated servers and Storage Boxes — is a different API behind a different credential, and nothing in this touches it. Said on the Preferences row and on the token step rather than left to be discovered by an account that has both. And there is no phone layout. This one would work on Android, unlike the ssh_config importer beside it: it is a token and a network request, the view model is shared already, and scoped storage has nothing to do with it. What is missing is a screen — the desktop draws a five-column table, and the phone version of that is a different layout rather than the same one narrower. Recorded in android-port.md and on the phone's own Preferences screen, because the note there previously read as though no import of any kind could work. ◆ TESTS, AND WHAT IS LEFT FOR A PERSON. HetznerCloudClientTests is 16 cases over the shapes the API actually returns, against a stubbed HttpMessageHandler: paging followed, the token in a header and never a query string, a refused token distinguished from a fault at Hetzner's end, a token with a line break refused before anything is sent, the /64, the private-network asymmetry, and a server the API described without a name. ShellFlowTests.Hetzner.cs is 11 more against this fixture's real vault, real cache and real server — split into a file of its own, as FakeVaultServer keeps its team endpoints. What they establish beyond the reader's own suite is the half it cannot reach: that reading stores nothing, that importing goes through the ordinary create-and-push path and lands on the server with nothing pressing Sync, that a server the keychain already has arrives unticked, and that the token does not outlive the request. Two layout cases measure both steps against the same content budget every other settings page gets, which is also what proves the markup binds to names that exist. Manual checks 2.11a–2.11d are what no test in this repository can do, because there is no account, no token and no network in the ordinary suite: a real project listed against the real console, a wrong token, the private choice, and what an imported host does when you connect to it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
155 lines
9.2 KiB
XML
155 lines
9.2 KiB
XML
<UserControl xmlns="https://github.com/avaloniaui"
|
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
|
x:Class="DodoSSH.Client.App.Views.SettingsPreferencesPage"
|
|
x:DataType="vm:MainWindowViewModel">
|
|
|
|
<!--
|
|
v5c: Preferences, against Settings-Preferences.dc.html — the old PreferencesScreen.axaml's real content,
|
|
restyled into the settings page idiom and split across two pages rather than one.
|
|
|
|
THIS MACHINE moved to the Security page whole: Windows Hello's register/"stop unlocking here" pair and
|
|
the no-TPM explanation are Security's UNLOCKING card now, one home rather than two — see the cross-
|
|
reference row at the foot of this page. Device name is not one of the rows that moved; it never existed
|
|
here at all, because there is no device-name setting to move. Logs record the machine name on their own.
|
|
|
|
TERMINAL keeps its one real setting, text size, and drops the design's other five — font, cursor,
|
|
scrollback, copy on select, terminal bell — which the renderer hard-codes. KEYCHAIN keeps Lock now,
|
|
Sync now/Sign in, and the importer rows; the design's own SECURITY card (clipboard-clear delay,
|
|
confirm-run-on-insert) has nothing behind either row and is not drawn here.
|
|
|
|
Two importer rows now rather than the one the old screen had: ~/.ssh/config, and a Hetzner Cloud project.
|
|
-->
|
|
|
|
<ScrollViewer>
|
|
<StackPanel MaxWidth="1100" Margin="40" HorizontalAlignment="Stretch">
|
|
|
|
<TextBlock Classes="settingstitle" Text="Preferences" />
|
|
|
|
<TextBlock Classes="settingssection" Text="TERMINAL" />
|
|
|
|
<Border Classes="settingscard">
|
|
<StackPanel>
|
|
|
|
<Border Classes="settingsrow last">
|
|
<Grid ColumnDefinitions="*,Auto">
|
|
<StackPanel Grid.Column="0" Spacing="6" Margin="0,0,16,0">
|
|
<TextBlock Classes="settingsrowtitle" Text="Text size" />
|
|
<TextBlock Classes="settingsrowcaption"
|
|
Text="How large a terminal draws, in pixels. Ctrl+plus and Ctrl+minus do the same while a terminal has focus, and Ctrl+0 puts it back. It resizes the grid rather than magnifying it, so the remote is told how many columns it now has — which is also why it stops before the columns run out." />
|
|
</StackPanel>
|
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Top">
|
|
<Button Width="36" Height="36" Classes="ghost" Padding="0"
|
|
Command="{Binding ShrinkTerminalFontCommand}"
|
|
IsEnabled="{Binding CanShrinkTerminalFont}"
|
|
ToolTip.Tip="Smaller · Ctrl+minus">
|
|
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="15" />
|
|
</Button>
|
|
<TextBlock Classes="mono" FontSize="13.5" MinWidth="46" VerticalAlignment="Center"
|
|
TextAlignment="Center" Foreground="{StaticResource Text}"
|
|
Text="{Binding TerminalFontSize, StringFormat={}{0} px}" />
|
|
<Button Width="36" Height="36" Classes="ghost" Padding="0"
|
|
Command="{Binding EnlargeTerminalFontCommand}"
|
|
IsEnabled="{Binding CanEnlargeTerminalFont}"
|
|
ToolTip.Tip="Larger · Ctrl+plus">
|
|
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="15" />
|
|
</Button>
|
|
<Button Classes="ghost" Height="36" Content="RESET"
|
|
Command="{Binding ResetTerminalFontCommand}"
|
|
ToolTip.Tip="Back to the size it ships at · Ctrl+0" />
|
|
</StackPanel>
|
|
</Grid>
|
|
</Border>
|
|
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<TextBlock Classes="settingssection" Text="KEYCHAIN" />
|
|
|
|
<Border Classes="settingscard">
|
|
<StackPanel>
|
|
|
|
<Border Classes="settingsrow">
|
|
<Grid ColumnDefinitions="*,Auto">
|
|
<StackPanel Grid.Column="0" Spacing="6" Margin="0,0,16,0">
|
|
<TextBlock Classes="settingsrowtitle" Text="Lock the keychain" />
|
|
<TextBlock Classes="settingsrowcaption"
|
|
Text="Closes the keychain and forgets every key it held. Shells you have open keep running and reappear when you unlock — locked describes the keychain, not this machine's access to your hosts." />
|
|
</StackPanel>
|
|
<Button Grid.Column="1" Classes="ghost" Height="38" Content="LOCK NOW"
|
|
Command="{Binding LockCommand}" />
|
|
</Grid>
|
|
</Border>
|
|
|
|
<Border Classes="settingsrow">
|
|
<Grid ColumnDefinitions="*,Auto">
|
|
<StackPanel Grid.Column="0" Spacing="6" Margin="0,0,16,0">
|
|
<TextBlock Classes="settingsrowtitle" Text="Synchronise" />
|
|
<TextBlock Classes="settingsrowcaption"
|
|
Text="Runs a pass now. One runs on its own when the keychain opens, straight after any change, and every minute while it stays open." />
|
|
</StackPanel>
|
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
|
<Button Classes="ghost" Height="38" Content="SIGN IN" Command="{Binding SignInCommand}"
|
|
IsVisible="{Binding !IsOnline}"
|
|
ToolTip.Tip="Opens your browser. Only needed when there is no remembered session to resume." />
|
|
<Button Classes="ghost" Height="38" Content="SYNC NOW" Command="{Binding Vault.SyncCommand}" />
|
|
</StackPanel>
|
|
</Grid>
|
|
</Border>
|
|
|
|
<Border Classes="settingsrow">
|
|
<Grid ColumnDefinitions="*,Auto">
|
|
<StackPanel Grid.Column="0" Spacing="6" Margin="0,0,16,0">
|
|
<TextBlock Classes="settingsrowtitle" Text="Import SSH config" />
|
|
<TextBlock Classes="settingsrowcaption"
|
|
Text="Reads this machine's ~/.ssh/config, shows what it found, and imports only what you approve. Nothing is stored during the scan, and no private key is read — where a key file is named, the path is recorded as a note." />
|
|
</StackPanel>
|
|
<Button Grid.Column="1" Classes="ghost" Height="38" Content="OPEN IMPORTER"
|
|
Command="{Binding ShowScreenCommand}"
|
|
CommandParameter="{x:Static vm:ShellScreen.Import}" />
|
|
</Grid>
|
|
</Border>
|
|
|
|
<!--
|
|
The second importer, beside the first rather than folded into it: they read different things
|
|
through different code, and one row that asked "from where?" would put a question in front of
|
|
the far more common answer. See HetznerImportScreen.axaml.
|
|
-->
|
|
<Border Classes="settingsrow last">
|
|
<Grid ColumnDefinitions="*,Auto">
|
|
<StackPanel Grid.Column="0" Spacing="6" Margin="0,0,16,0">
|
|
<TextBlock Classes="settingsrowtitle" Text="Import from Hetzner Cloud" />
|
|
<TextBlock Classes="settingsrowcaption"
|
|
Text="Lists the servers in one Hetzner Cloud project and imports the ones you tick. Needs a read-only API token, which is used for a single request and never stored; a token belongs to one project, so an account with several runs this once each. No key can come with them — Hetzner has never held the private half of one — so imported hosts ask for a password until you bind them to a key in your keychain. Dedicated servers behind Hetzner's Robot are not covered." />
|
|
</StackPanel>
|
|
<Button Grid.Column="1" Classes="ghost" Height="38" Content="OPEN WIZARD"
|
|
Command="{Binding ShowScreenCommand}"
|
|
CommandParameter="{x:Static vm:ShellScreen.HetznerImport}" />
|
|
</Grid>
|
|
</Border>
|
|
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<TextBlock Classes="settingssection" Text="NOT BUILT YET" />
|
|
|
|
<Border Classes="settingscard">
|
|
<StackPanel Margin="24,20" Spacing="10">
|
|
<TextBlock Classes="settingsrowcaption"
|
|
Text="Unlocking with Windows Hello moved to Security — it registers this machine rather than a preference of this screen's, and Security is where the rest of this machine's trust facts live." />
|
|
<TextBlock Classes="settingsrowcaption"
|
|
Text="Terminal font, cursor and scrollback — the renderer hard-codes those three. Text size above is the one that is not." />
|
|
<TextBlock Classes="settingsrowcaption"
|
|
Text="Copy on select and the terminal bell — neither is wired to anything the renderer does." />
|
|
<TextBlock Classes="settingsrowcaption"
|
|
Text="Clearing the clipboard after copying a secret, and confirming a snippet that runs on insert — neither exists; a copied secret stays on the clipboard until something else replaces it." />
|
|
<TextBlock Classes="settingsrowcaption"
|
|
Text="A device name — there is no such setting. Logs record this machine's own name on their own." />
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
</StackPanel>
|
|
</ScrollViewer>
|
|
|
|
</UserControl>
|