diff --git a/README.md b/README.md index 7410447..1fb7d3d 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,8 @@ src/ DodoSSH.Client.Terminal the loopback data plane and credit-based flow control DodoSSH.Client.Transfer the transfer queue, part files and resume, and the local file listing DodoSSH.Client.ObjectStore S3-compatible buckets, behind the same interface as SFTP - DodoSSH.Client.Import reading ~/.ssh/config, and the keys it names when asked + DodoSSH.Client.Import reading ~/.ssh/config and the keys it names when asked; reading a Hetzner + Cloud project over its API DodoSSH.Client.Shell the view models both heads drive, the renderer's files, the palette DodoSSH.Client.App the desktop head: its views, and its Windows, macOS and Linux integration DodoSSH.Client.Android the phone head: its views, and its Android integration diff --git a/docs/android-port.md b/docs/android-port.md index bf044ba..31c7e51 100644 --- a/docs/android-port.md +++ b/docs/android-port.md @@ -755,6 +755,12 @@ Recorded so they are choices rather than accidents. Any of them is cheap to revi local-and-non-roaming requirement more cleanly than any desktop platform does. - **The device name** on log entries comes from the head, not `Environment.MachineName`, which returns something like `localhost` on Android and would make every entry from a phone indistinguishable. +- **The Hetzner Cloud importer is not ported, and it is the one gap here that is not a platform limit.** + Its view model is in `DodoSSH.Client.Shell` like every other and its reader is an `HttpClient` call, so + both halves already run on a phone — unlike the `~/.ssh/config` importer beside it, which has no file to + read under scoped storage and never will. What is missing is a layout: the desktop draws a five-column + table of servers, and a phone version of that is a different screen rather than the same one narrower. + Said on the Preferences screen rather than left to be discovered. - **`NativeKeyboardFocus` is not ported.** It exists for a documented Win32 asymmetry — focus crosses into WebView2 but does not come back — and Android's focus model is different enough that the problem should be confirmed to exist before anything is written to solve it. diff --git a/docs/manual-checks.md b/docs/manual-checks.md index 97a9c47..836458f 100644 --- a/docs/manual-checks.md +++ b/docs/manual-checks.md @@ -165,7 +165,9 @@ ordinary nav rail destination does through `MainWindowViewModel.ShowScreen`. An comes up over settings mode the same way it would over any other screen. **Failure means:** a back button that never changes label is `SettingsTitleBar.axaml`'s two buttons both -bound to the same side of `IsImportOpen`. SettingsNav lighting a different row while the importer is up is +bound to the same side of `IsImporterOpen` — which is the flag now, rather than `IsImportOpen`, because +there are two importers over that page and the bar behaves identically for both. SettingsNav lighting a +different row while an importer is up is `MainWindowViewModel.IsSettingsPreferencesPage` reading `IsImportOpen` when it must not — `Import.dc.html` draws the rail unmoved on purpose; see the remark on `IsImportOpen`. Ctrl+K doing nothing over the importer would be a guard added to `OnKeyDown` for `IsSettingsMode` that the design never asked for @@ -410,6 +412,74 @@ machine. That is the ordinary case rather than an error. --- +## Phase 2 — Importing from Hetzner Cloud + +The reader has its own suite over the shapes the API returns, and the end-to-end path is covered by +`ImportingFromHetzner_ShowsTheProjectFirstAndThenStoresWhatWasTicked` against a stubbed transport. What no +test in this repository does is talk to Hetzner — there is no account, no token and no network in the +ordinary suite — so everything below needs a real project. + +### 2.11a A read-only token lists your project + +Make a token in the Hetzner console under **Security → API tokens** with **Read** permission only. Settings +→ Preferences → **OPEN WIZARD**, paste it, press **READ THE PROJECT**. + +**Pass:** every server in that project is listed, with the address, the plan and the location the Hetzner +console shows for it. The token box is **empty** afterwards — it is used for one request and dropped — and +the Hosts screen is unchanged, because reading writes nothing. + +**What to look for specifically:** +- A project with more than fifty servers must list all of them. Fifty is Hetzner's page size, and a list + that stops there exactly is the paginator not being followed. +- A server with no public IPv4 should show the first address of its `/64` with a warning chip saying so. + Check that address against what the server actually answers on — the convention is Hetzner's own images, + not a fact the API states, and a machine whose operator moved it elsewhere in the `/64` will not answer. +- A server the console shows as **off** should still be listed, with `off` in the STATE column. + +**Failure means:** the importer and the console disagree about what the project holds, which produces +bookmarks that dial the wrong machine. Worse than an import that refused. + +### 2.11b A wrong token says so, and says where tokens come from + +Paste anything that is not a valid token — a truncated one is the realistic case, since the console shows +the value once. + +**Pass:** the wizard stays on the token step, the message names the Hetzner console and the Security → API +tokens page, and the box is cleared for a fresh paste. Nothing has been stored. + +Now try a token from a **different** project. + +**Pass:** it lists that project instead. A token belongs to one project and there is no API call that will +say which — an account with several projects runs this once per project, which the note above the box says. + +### 2.11c The private-network choice does not fall back + +With a project holding at least one server on a private network and one on none, press **PRIVATE NETWORK**. + +**Pass:** the server with no private network shows an em dash for its address, a chip reading *no address of +that kind*, and a tickbox that cannot be ticked — including by the tick-all box in the header. The other +one shows its `10.x` address. + +**Failure means:** a public address quietly stored under a choice that asked to stay off the internet. This +is the one asymmetry in how addresses resolve and it is deliberate; v4 and v6 fall back to each other, +private does not. + +### 2.11d Imported hosts are correct, and ask for a password + +Import a couple, then open one on the Hosts screen. + +**Pass:** the address, port and login user are what the wizard showed, and the notes carry the server id, +the plan, the location and the image — and **not** the power state, which would be stale the moment somebody +pressed a button in the console. Connecting asks for a **password**: no key came with these hosts, because +Hetzner has never held the private half of one. Bind it to a key in your keychain and it connects. + +Now go back to the list and import the same project again. + +**Pass:** the ones already stored are marked *already here* and arrive unticked. Importing one twice is +allowed and takes a click. + +--- + ## Phase 2 — Drag and drop on the SFTP page **This is the least-covered thing in the repository, and unavoidably so.** Headless Avalonia has no native diff --git a/src/DodoSSH.Client.Android/Views/PreferencesScreen.axaml b/src/DodoSSH.Client.Android/Views/PreferencesScreen.axaml index 5009514..a9ba586 100644 --- a/src/DodoSSH.Client.Android/Views/PreferencesScreen.axaml +++ b/src/DodoSSH.Client.Android/Views/PreferencesScreen.axaml @@ -163,6 +163,8 @@ Text="Text size is on the terminal itself, where the text is — the phone puts A− and A+ in the shell's own bar rather than a step away from what they resize." /> + diff --git a/src/DodoSSH.Client.App/Views/HetznerImportScreen.axaml b/src/DodoSSH.Client.App/Views/HetznerImportScreen.axaml new file mode 100644 index 0000000..8416b52 --- /dev/null +++ b/src/DodoSSH.Client.App/Views/HetznerImportScreen.axaml @@ -0,0 +1,314 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +