Let the phone delete a host, and move or remove a group

The desktop gained three things the phone did not follow: moving a group to
another vault, the second question asking whether a group's deletion takes its
hosts with it, and — since long before either — deleting a host at all. What
that left was a head whose v3 + can fill a keychain and whose editors can
correct one, with no way to empty either.

The commands could not simply be bound. DeleteGroup and MoveGroup aim at
GroupTarget, which is the selected card or the open group, and the phone has
neither: its list draws headings, and a heading's selection deliberately bounces
back to the host. Called bare on that head they would have returned having done
nothing — a DELETE that appears to have been pressed and has not. Both now take
the row and fall back to GroupTarget for the desktop's menu, and
ConfirmMoveGroupAsync resolves from the panel's own movingGroupId rather than
from the selection, which is also the honester answer on the desktop: what moves
is the shelf the panel was opened on.

The heading's pencil became a menu. Three icons after a chevron, a name, a vault
badge and a count is what would be left of the name at 360dp, so the ⋯ raises
the add sheet's shape carrying Edit, Move to another vault, a rule, and Delete —
the desktop's card menu, in the one idiom this screen already has. It does not
carry Open: the desktop's grid holds one level of the group tree and this list
holds all of it flattened, so there is nowhere to open a group into.

DELETE under a host sits on a row of its own beneath EDIT and MOVE rather than
beside them. A phone has no hover and no tooltip, so where a thumb lands is the
only thing separating a destructive control from an ordinary one. Both questions
take the controls that asked them — ShowsConnectControls, which is the phone's
half of the rule ShowsHostPaneActions already carries for the desktop's drawer —
so DELETE cannot be pressed a second time underneath its own confirmation.

Preferences gained the running version, and the sentence saying this head does
not replace itself and that no DodoSSH server will ever offer one. It reads
Updates.CurrentVersion off the same view model the desktop's UPDATES section
does, over the null channel that reports itself unsupported.

Nothing was needed for the realtime push: it is composed in ServerConnection,
which both heads use.

Seven tests, all phone-shaped — a group acted on with nothing selected, the menu
waved away leaving nothing armed, the ungrouped heading raising none, and the
bar's three states. The rectangles remain unmeasurable for the reason phase 8
gives; the checks for them are 8.10 to 8.13 and 13.6.
This commit is contained in:
2026-08-04 19:58:10 +02:00
parent e923b12b7f
commit 50fa6fba38
10 changed files with 814 additions and 52 deletions
+88 -4
View File
@@ -847,10 +847,10 @@ add a bucket — then come back.
---
## Phase 8 — Adding hosts on the phone
## Phase 8 — Adding and removing on the phone's host list
Nine checks, and the reason there are nine rather than none is worth stating: **the layout suite cannot see
any of this and structurally never will.** `DodoSSH.Client.App.Layout.Tests` targets `net10.0` and
Thirteen checks, and the reason there are thirteen rather than none is worth stating: **the layout suite
cannot see any of this and structurally never will.** `DodoSSH.Client.App.Layout.Tests` targets `net10.0` and
`DodoSSH.Client.Android` targets `net10.0-android`, so a project reference is impossible; Avalonia's
application, dispatcher and platform are one-shot process globals, so a second head cannot share the
process either; and the phone's numbers only mean anything measured under the phone's own style graph.
@@ -936,15 +936,87 @@ Tap + → New host, then use the back gesture.
**Pass:** the editor closes and the list is back, with the boxes cleared. Back again leaves the application
— HOSTS is the screen the app opens on and back from it should exit, as it always did.
Repeat with the group menu: tap the `⋯` on a group heading, then back.
**Pass:** the menu closes and nothing is armed behind it — no editor, no question, no move panel.
### 8.9 Every target is a thumb's size
Walk the sheet's two rows, the editor's fields, both pickers, the checkbox and the four buttons.
Walk both sheets' rows, the editor's fields, both pickers, the checkbox and the four buttons.
**Pass:** nothing is under 44 effective pixels tall.
**Failure means:** a control took its size from its glyph. The desktop suite's only tap-target assertion has
a floor of 20 pixels — set for a mouse — so copying it here would certify a target no thumb can hit.
### 8.10 The group menu names its group, and the heading still fits
Needs a vault with at least one group holding a host. On HOSTS, look at the group's heading row first.
**Pass:** chevron, name, count and a `⋯` all on one line, with the name trimmed rather than pushing the
count off the right edge. On a shared session the vault badge is between the name and the count and the name
gives way before the badge does.
Tap the `⋯`.
**Pass:** a sheet rises carrying **GROUP** and the group's own name, then three rows — Edit, Move to another
vault, a rule, and Delete in the danger colour — each with a sentence under it, and CANCEL. Tapping the
scrim closes it.
**Failure means:** if the name is missing, the sheet is bound to the wrong thing; the whole point of naming
it is that the heading it was raised from may have scrolled away under the scrim.
### 8.11 Deleting a group asks the second question
`⋯` → Delete on a group with at least two hosts filed under it.
**Pass:** a question above the list, naming the group, saying how many hosts are filed under it and that
left alone they move to UNGROUPED — with an **unticked** box offering to delete them as well, below the
count and not above it. DELETE and KEEP under that.
Press KEEP, then raise the question again on a *different* group.
**Pass:** the box is unticked again. A tick carried over from the last question would delete a second
group's machines on the strength of a decision about the first.
Now tick it and press DELETE.
**Pass:** the group and its hosts are gone. Untick it on a third group and the hosts survive under UNGROUPED.
### 8.12 Moving a group names the shelf · **needs a second writable vault**
`⋯` → Move to another vault.
**Pass:** a panel above the list reading **MOVE GROUP** and the group's name, a vault picker that does not
offer the vault it is already in, the paragraph about what stays behind, and MOVE / CANCEL.
Press MOVE.
**Pass:** the group, the groups nested inside it and every host under any of them are in the other vault,
the group is at the top level, and the status line says so.
**Failure means:** if MOVE appears to do nothing at all, the command is reading the desktop's group
selection, which this head never sets. That is the exact failure the header-aimed commands exist to prevent
— see `VaultViewModel.MoveGroupFromHeading`.
With only one writable vault, the entry answers on the status line instead and no panel opens. That is the
intended answer, not a failure.
### 8.13 DELETE under a host is not beside EDIT
Select a host so the connect bar rises.
**Pass:** CONNECT, then EDIT and MOVE side by side, then DELETE full width in the danger colour on a row of
its own beneath them. On a host with nowhere to move to, MOVE is absent and the row is EDIT alone.
Press DELETE.
**Pass:** the question takes the whole bar — no CONNECT, no EDIT, no second DELETE underneath it — and KEEP
puts them back with the host still there.
**Failure means:** a bar that kept its buttons under the question is one where DELETE can be pressed twice,
the second press landing on whatever the list rebuilt underneath.
---
## Phase 9 — Tag chips and the picker
@@ -1406,6 +1478,18 @@ rather than two. There is no confirmation prompt, deliberately.
**Failure means:** a phone that still unlocks itself after this is the local half not happening, which is the
half that matters when the handset is the thing that was lost.
### 13.6 The phone says which build it is
Scroll the same screen to **THIS BUILD**.
**Pass:** a VERSION row carrying a real version rather than `1.0.0`, matching the tag the APK was built
from, with the paragraph under it saying this head does not replace itself and that no DodoSSH server will
ever offer you a newer one.
**Failure means:** `1.0.0` is MinVer answering from a checkout with no tags — see check 16.5 for the same
failure on the desktop. A missing paragraph means `Updates.IsUnsupported` came back false on a head that
has no updater at all, which would be the null channel not being the one composed.
---
## Phase 14 — Moving files to and from the phone's remote