From 50fa6fba384b0d07538d77f20260b21d5c75013b Mon Sep 17 00:00:00 2001 From: Jaap-Jan de Wit | DodoTech Date: Tue, 4 Aug 2026 19:58:10 +0200 Subject: [PATCH] Let the phone delete a host, and move or remove a group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- README.md | 14 +- docs/android-port.md | 16 +- docs/design-import-gaps.md | 3 + docs/manual-checks.md | 92 ++++++- .../Views/HostsScreen.axaml | 232 +++++++++++++++++- .../Views/PhoneShell.axaml.cs | 14 +- .../Views/PreferencesScreen.axaml | 31 +++ .../ViewModels/VaultViewModel.cs | 232 +++++++++++++++--- .../ShellFlowTests.cs | 174 +++++++++++++ .../VaultSharingTests.cs | 58 +++++ 10 files changed, 814 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 3435b71..f8076f8 100644 --- a/README.md +++ b/README.md @@ -390,8 +390,9 @@ to" picker on the Keychain screen and cannot be moved yet. **A group can be moved too, and it takes its contents with it** — "Move to another vault…" on the group card's right-click menu, beside Open, Edit and Delete, which is the whole of what can be done to a group on -the desktop. That is the desktop only, because the phone draws groups as headings in the host list and has -never had a way to delete or move one. It is the same re-seal +the desktop. The phone has the same three on a sheet raised from the group's heading, since it draws no +cards and has no right-click; it does not have Open, because its list shows the whole tree flat and there is +nothing to open a group into. It is the same re-seal and tombstone underneath, applied to every item involved: the group, the groups nested inside it, and every host filed under any of them, each taking a new id in the destination. Moving less than that was never coherent — the machines and the child groups are items of the vault the group is leaving, so a group that @@ -500,9 +501,12 @@ transfer starts, the download runs into the cache, and the finished bytes are co chose. That order has one visible cost, and the screen says it rather than leaving it to be discovered: the picker creates the file when you dismiss it, so a download that then fails leaves an empty one there. The alternative is a picker that appears minutes later over whatever you moved on to — and often while the app -is in the background, where Android will not show one at all. Hosts and groups are made and corrected here now, from -a floating + on the Hosts screen, and both editors are cards in the list's own row rather than dialogs, so -the form never covers the thing it is about. The keychain has no editor of its own: SSH keys and buckets are +is in the background, where Android will not show one at all. Hosts and groups are made, corrected and taken +away here now, from a floating + on the Hosts screen and a menu on each group's heading, and both editors are +cards in the list's own row rather than dialogs, so the form never covers the thing it is about. Every +question a deletion asks is the desktop's, counted the same way and drawn in the place the buttons that +asked it were — including the tick deciding whether a group's machines go with it. The keychain has no +editor of its own: SSH keys and buckets are created on the desktop and sync down, and the phone will delete an item — behind the same counted confirmation — without offering to change it. What this head does make, it makes where the need arises rather than in an editor: a tag from inside a host's editor, and a credential from the connect bar's diff --git a/docs/android-port.md b/docs/android-port.md index dbebd99..4ae7fc9 100644 --- a/docs/android-port.md +++ b/docs/android-port.md @@ -604,8 +604,20 @@ What is left, in the order it matters: the decision above for the shape and for the one thing about it a person will notice — an empty file where a failed download was pointed. What is *not* built is a folder picker for several downloads at once: the save picker names one destination, so SAVE FILE takes the selected row. -- **Editors.** There is no host editor and no keychain item editor on the phone, so both are create-on- - desktop-and-sync. That is why the v2 design's `+` buttons on HOSTS and on the keychain are not drawn. +- ~~**Editors.**~~ **Half built.** The v3 hosts screen has both a host editor and a group editor — cards in + the list's own row, swapped for the list rather than stacked over it — reached from the design's floating + `+` through a sheet asking which of the two is being added. The keychain still has none, so keys, + passwords and buckets remain create-on-desktop-and-sync; its one action is DELETE. + + **And everything can now be taken away as well as made.** DELETE under a host, and a group menu on the + heading carrying Edit, Move to another vault and Delete — the desktop's card menu, as a bottom sheet, + because this head draws no group cards and has no right-click. Both questions replace the controls that + asked them rather than stacking over them, which is the rule this document's step 7 already recorded for + the keychain's. A screen that could fill a keychain and never empty one was the shape before it. + + The one entry the phone's menu does not carry is Open. The desktop's grid holds one level of the group + tree at a time and this list holds all of it flattened, so there is nothing here to open a group *into* — + see the correction under *Sidebar* above, which is the same difference stated from the other end. - **Pins and import**, which v2 does not draw either. Teams *is* drawn, behind MORE — it was the one of the three whose view model needed nothing new on the phone, because none of that screen is vault content. - **The App Link upgrade**, unchanged from step 5. diff --git a/docs/design-import-gaps.md b/docs/design-import-gaps.md index 131b6c3..c307536 100644 --- a/docs/design-import-gaps.md +++ b/docs/design-import-gaps.md @@ -52,6 +52,9 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference > | A 14px rounded inset around the terminal | Not drawn. The renderer is a native child view composited above everything Avalonia draws, so a rounded frame behind it clips nothing. What the design was after comes from the page's own background, which is the design's `#171A26`. | > | Instrument Sans / Fira Code | Inter and the system monospace stack, as before and for the same reason — see the note on `MonoFont` in `Palette.axaml`. | > | A theme toggle (`☀`) in the header, and the desktop v2's light theme | Omitted. There is one theme by decision, and a switch with one position is a control that does nothing. | +> | Nothing at all on a group beyond its heading | ◆ **A menu on the heading, which is the second thing on this list the design had no slot for.** The desktop puts Edit, Move to another vault and Delete on the group's own card; this head draws no group cards and has no right-click, so the `⋯` raises the add sheet's shape with the same three entries in the same order — Move above the rule because it is not a deletion, Delete below it. Open is the one entry it does not carry: the desktop's grid holds one level of the tree and this list holds all of it flattened, so there is nowhere to open a group *into*. It was a pencil alone while Edit was the only thing behind it. | +> | Nothing that removes a host | ◆ **DELETE, on a row of its own under EDIT and MOVE.** A phone has no hover and no tooltip, so where a thumb lands is the only thing separating a destructive button from an ordinary one — full width in the danger colour, below both, is the same arrangement KEYCHAIN uses. The counted question takes the bar's controls in place rather than stacking over them, which is what stops DELETE being pressed a second time under its own confirmation. Until v3 this head could fill a keychain and never empty one. | +> | PREFERENCES, which v2 draws as a list of settings | ◆ **One setting, one fact, and a paragraph of absences.** The setting is whether this phone may unlock with a fingerprint — see the screen. The fact is the running version, which matters more here than on the desktop: there it sits beside a CHECK NOW that will say whether it is current, and this head does not replace itself at all. What it says instead is where a newer one comes from, which is ADR 0011 rule 2 in plain words — never from the server you sign in to. | > | TERMINAL's empty state: a sentence saying nothing is open | ◆ **More than the design asked for, which is rare enough on this list to say out loud.** The tab is called **Connections** and with nothing running the screen is where one is made: a box taking `user@host` or `user@host:port` with a password, and the machines most recently connected to underneath it, read from the vault's own connection log. The box is the only path in the product to a machine that is not in the keychain — the design assumes every connection starts from a saved host, and the case it has no answer for is an address somebody was handed five minutes ago. Nothing typed there is saved, and the screen says so. | > > ## The desktop's v2 diff --git a/docs/manual-checks.md b/docs/manual-checks.md index a8f5109..f7a1b9c 100644 --- a/docs/manual-checks.md +++ b/docs/manual-checks.md @@ -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 diff --git a/src/DodoSSH.Client.Android/Views/HostsScreen.axaml b/src/DodoSSH.Client.Android/Views/HostsScreen.axaml index 684f2fd..ad837cd 100644 --- a/src/DodoSSH.Client.Android/Views/HostsScreen.axaml +++ b/src/DodoSSH.Client.Android/Views/HostsScreen.axaml @@ -89,6 +89,94 @@ + + + + + + + + + + + + + + + + + + + + + + +