Record v5c in the design-import log, and true up the manual checks
ci / build and test (push) Failing after 2m14s
ci / desktop nightly (push) Skipped
ci / api image (push) Skipped
ci / android head (push) Successful in 3m27s

This commit is contained in:
2026-08-08 14:17:20 +02:00
parent bb2f973687
commit c59b517fdf
2 changed files with 185 additions and 52 deletions
+126 -50
View File
@@ -115,6 +115,61 @@ vault that authenticates with a key filed in the switched-off one still connects
`VaultViewModel.IsVaultShown` for the list, and `VaultVisibilityTests` for the same assertions made against
view models — this check is the version with a real connection on the end of it.
### 1.8 Entering and leaving settings mode
From HOSTS, press the user chip at the foot of the nav rail and choose **Settings**. Then repeat from a
fresh open, choosing **Vaults**, and again choosing **Preferences**.
**Pass:** each of the three swaps the whole window's chrome — the titlebar becomes the 53px "Back to
application" bar, the ordinary nav rail is replaced by the 340px SettingsNav, and the status bar and the
update banner (if either was showing) are both gone. **Settings** lands on General; **Vaults** and
**Preferences** land on their own pages, each lit in SettingsNav's own list — General/Vaults/Account under
SETTINGS, Security/Preferences/Groups/Tags under CUSTOMIZE, Logout pinned below both. Click every one of the
seven rows in turn: each swaps the content column without leaving settings mode or touching the titlebar.
Now open a terminal, leave it showing, and open Settings from the user chip again. Press **Back to
application** — or, without touching anything else, press Esc.
**Pass:** both return to exactly the screen that was showing before Settings was opened — the terminal,
still running, rather than HOSTS or anywhere else. Switching between settings pages first (General → Vaults
→ Preferences) does not change what "back" goes back to; only the screen open the moment Settings was
**first** entered does.
**Failure means:** a return to HOSTS regardless of where Settings was opened from is
`MainWindowViewModel.settingsReturnScreen` being recaptured on every `EnterSettings` call rather than only on
the way in from outside settings mode — see that field's own remark. A status bar or update banner still
visible in settings mode is `MainWindow.axaml`'s two bottom rows no longer reading `!IsSettingsMode`. Esc
doing nothing is `MainWindow.axaml.cs`'s `OnKeyDown` no longer checking `IsSettingsMode` after the palette's
own branch.
### 1.9 The importer's own home, and what still reaches through settings mode
From Settings → Preferences, press **OPEN IMPORTER**.
**Pass:** the titlebar's back button relabels itself "Back to preferences" — not "Back to application" — and
SettingsNav stays lit on **Preferences** the whole time; the importer is drawn over the Preferences page
rather than being a destination of its own. Press SCAN AGAIN and let it read your `~/.ssh/config`.
Press **Back to preferences** (or Esc). **Pass:** the Preferences page is back, still inside settings mode,
and the titlebar's label reverts. Press Esc a second time (or **Back to application**): now settings mode
itself closes, back to whatever screen was open before Settings was entered — the same two-deep
"closest thing first" order the titlebar's own two buttons and `MainWindowViewModel.OpenImport`/`CloseImport`
follow.
**Then, with the importer open, press Ctrl+K** and connect to a host from the palette.
**Pass:** it works exactly as it does anywhere else in the application — the palette opens over the
importer, and connecting leaves settings mode outright for the new terminal, the same way choosing any
ordinary nav rail destination does through `MainWindowViewModel.ShowScreen`. An unapproved host's key card
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
`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
and this application's own quick-connect card was built to reach past.
---
## Phase 2 — Known Hosts as its own page
@@ -200,7 +255,7 @@ The parser has 22 cases over the shapes a real file contains, and the end-to-end
### 2.7 Scan your own `~/.ssh/config` and read the preview against the file
Preferences → IMPORT HOSTS → SCAN. Do not press import yet.
Settings → Preferences → OPEN IMPORTER, then SCAN AGAIN. Do not press Import N hosts yet.
**Pass:** every entry you would expect is listed, with the address and port you expect, and the warnings
above the table account for anything missing.
@@ -364,61 +419,81 @@ card, nothing saying the hosts are unfiled.
**Failure means:** the "invisible until used" property is gone, and every existing user gets a section they
did not ask for. `HasGroups` is what hides the row of group cards; `HostRowViewModel.HasGroup` hides the chip.
### 3.2 Filing hosts, and the grid being one level at a time
### 3.2 Filing hosts, and the board drawing every group at once
Make two groups and file some hosts into each through the host editor, leaving at least one host unfiled.
Make two groups — the Group ▾ flyout's "New group…" — and file some hosts into each, once through a host's
own editor and its GROUP picker, and once by ticking several hosts and choosing **Change group…** from the
right-click menu. Leave at least one host unfiled.
**Pass:** the filed hosts **leave the grid** as they are filed — a group is a place, not a label, and the
grid holds one level of it the way a directory pane holds one directory. What is left is the unfiled hosts.
**There is no heading and no fold on the desktop** — the headings, their chevrons and UNGROUPED are the
phone's, whose list has no room for a row of group cards and draws the whole tree flat instead. With every
host filed, the grid says so in a sentence rather than sitting empty.
**Pass:** every group is a heading on the board **at once** — in label order, "No group" first where it is
occupied — with its own hosts underneath. There is no double-click, no card to open a group into and no
breadcrumb trail: v3's one-level-at-a-time grid is gone outright, and every section is on screen from the
moment the board is. A host filed under `production` is drawn once, under that heading, and nowhere else on
the board.
**Then press a group card once.** It is marked as chosen and **nothing else happens** — the grid is still the
level it was, and no buttons appear beside the GROUPS heading: editing and deleting a group are on the card's
own right-click menu, which is 7.9. **Then double-press it.** The group opens: its hosts
are the grid, the trail above the cards reads `ALL HOSTS <name> `, each card carrying the group's name as
an accent chip, and the card grid shows what is *inside* that group rather than every group in the keychain.
Pressing ALL HOSTS goes back to the outermost level.
**Then press the chevron beside one heading.** It folds — the heading stays, with its live count, and its
cards go — and pressing it again brings the cards back. **Then press "Collapse all"**, drawn on the first
heading only. It folds every section at once and its own label swaps to "Expand all"; opening any one heading
by hand swaps the label back to "Collapse all".
**Then type a filed host's name into the find box at the top.** It is found from the outermost level,
wherever it was filed and however deep, with the chip on its card saying which group it came out of. Clearing
the box puts it away again. Inside a group the same box reaches that group and everything under it, and the
empty-grid sentence there offers ALL HOSTS as the way to widen it.
**Then type a filed host's name into the find box at the top, with its heading folded.** **Pass:** the
heading's own count narrows to match — the count is read off the set the find box has already narrowed,
before the fold is looked at — but the cards themselves stay hidden until that heading is reopened; open it
and the narrowed set is there. Clear the box and every count returns.
**Failure means:** if one press still narrows the grid, the card `ListBox` is bound to the wrong property —
`SelectedItem` is `SelectedGroup`, and only `OpenGroupCommand` writes `GroupFilter`. Filed hosts still on the
outermost level is `Matches` no longer comparing the host's group to the open one. A search that cannot find
a filed host is worse than either: it is the box answering "no host matches that" about a machine the
keychain has got. A full-width bar with a chevron between the cards is the old grouping coming back through
`SidebarRows`; the desktop grid binds `VisibleHosts`. See `HostsScreen.axaml`.
**Failure means:** a host drawn under a heading that is not its own, or under two at once, is
`VaultViewModel.AddFlatSection`'s membership test reading the wrong field for that section —
`host.Host.GroupId == group.EntityId` for a named heading, everything else for "No group". A chevron that
folds every section rather than the one it sits beside is `ToggleGroupCommand` being handed the board rather
than the single `SidebarGroupHeader` it was pressed on. A count that does not move with the find box is
`MatchesHostBoardFilters` not running before `HostSections` is rebuilt — see
`VaultViewModel.RebuildHostSections` and `OnHostFilterChanged`.
### 3.2a A group inside a group, and the way back out
### 3.2a A group nested under another, and what a host filed under it inherits
Make two groups and file one under the other with the parent picker in the group editor.
Make an outer group with a default port and no default username, then make a second group whose editor's
PARENT picker points at the outer one, and give this inner group no default port of its own. File a host
under the inner group with nothing set on the host itself.
**Pass:** only the outer group has a card to start with. Double-press it and the inner one is the only card
shown, with the trail reading `ALL HOSTS <outer> `. Double-press that, and the cards disappear entirely —
it has nothing inside it — while the trail stays. Pressing the **middle** crumb goes back one level rather
than all the way out, which is also how a group with nothing inside it is renamed: back out to the level
where it has a card, and right-click that.
**Pass:** both groups are their own headings on the board, side by side — nesting is never drawn on the board
itself, only carried in the group editor's own PARENT field. The inner heading's count is only the hosts
filed directly under it, and the outer's is only the hosts filed directly under the outer one; the host filed
under the inner group is not counted on the outer heading. Connect the host anyway: it dials the **outer**
group's port. `HostInheritance.Chain` walks past the inner group's own silence on that field rather than
stopping at the nearest group regardless of whether it answered. Give the inner group its own default port
and reconnect: the host now dials that one instead, because the nearer group's answer wins once there is one.
**Failure means:** cards for groups that are not at this level is `VisibleGroups` having been bound past —
the flat `Groups` is the phone's and the lookups'. A group that cannot be reached at all is worse and is the
case `EffectiveParents` promotes: see 3.4a.
**Then open the inner group's own editor and look at its PARENT picker.** The outer group is offered. **Then
open the outer group's own editor and look at its PARENT picker.** The inner group is **not** offered — a
group found by walking down from the outer one is refused as its own parent, which is what keeps this one
machine, acting alone, from building a cycle. (Two machines can still build one offline between them; that is
3.4a, below.)
### 3.2b Making something while standing inside a group
**Failure means:** a host that dials 22 with the outer group's port set is the chain stopping at the first
group above the host rather than reading each field independently from the nearest group that states it — see
`HostInheritance.Resolve`'s own remarks. A parent picker offering a group's own descendant is
`VaultViewModel.BuildGroupParentChoices` walking the wrong direction: each candidate has to be walked
*upward*, through `HostInheritance.Chain`, to see whether it passes through the group being edited — a
downward index of children is what the alternative would need, and this view model keeps none.
Open a group, then press **+ NEW HOST**, and afterwards **+ NEW GROUP**.
### 3.2b Making a host or a group inherits nothing from where the board is scrolled
**Pass:** the host editor opens with that group already chosen in its group picker, and the saved host is on
the screen it was made on rather than somewhere the trail is not. The group editor likewise opens with that
group as its parent, so the new group is a card inside the one that is open.
Scroll the board so one group's section fills the window, or fold every other section away, then press
**+ New host**. Afterwards, open the Group ▾ flyout and press **New group…**.
**Failure means:** anything created inside a group disappearing the moment it is saved. That is the papercut
a level-at-a-time grid comes with, and `NewHost` / `NewGroup` are where it is answered. Note the deliberate
difference between them: the host editor also takes a merely *selected* card as its group, the group editor
takes only the group that is open.
**Pass:** the host editor opens with its GROUP picker on **"No group"**, and the group editor opens with its
PARENT on **"No group"** too — neither reads anything from which heading happens to be on screen or scrolled
to. This is deliberate: the level-at-a-time grid these two commands used to inherit an "open group" from is
gone, and nothing on the flat board replaced that context. Pick a group by hand in either picker and it
stays picked — the host or the new group lands there once saved.
**Failure means:** an editor that opens already filed under whichever section happened to be on screen is
`VaultViewModel.GroupTarget` — the fallback both `NewHost` and `NewGroup` still read — having been wired to a
live selection again. Read that property's own remarks before treating this as a regression: it is an alias
for the single-select `GroupFilter`, which nothing in the current toolbar ever assigns any more — the Group ▾
flyout ticks a set, `checkedGroupFilterIds`, a different field entirely — so today the fallback is dead code
rather than a path either `+` button takes. Restoring it as live board context would make both commands
context-sensitive in a way nothing on this board signals before the fact.
### 3.3 Deleting a group with hosts in it
@@ -1618,7 +1693,7 @@ newcomer **must not have signed in to this deployment before** — 12.1 is about
### 12.1 An address with no account is refused, and joins nothing when it later signs in · **the one worth the most care**
1. Sign in as `alice`, make a vault on the VAULTS screen, and select it.
1. Sign in as `alice`, go to Settings → Vaults, make a vault there, and select it.
2. Add `bob@example.com` as a Member, with Bob having never signed in here.
3. **Pass:** it is refused. The status line names the address and says to ask them to sign in to this
server once and then add them. **Nothing on the screen should suggest anything is pending** — no
@@ -1716,7 +1791,7 @@ vault is now flagged for rekey, the transfer is removing the outgoing owner rath
Look for a way to remove a vault, on both heads.
**Pass:** there is none, and the VAULTS screen says why in a sentence: nothing in this product removes a
**Pass:** there is none, and Settings → Vaults says why in a sentence: nothing in this product removes a
vault, and the server refuses to archive the membership list behind one while it exists. Archiving that
list is still reachable over the API, and the endpoint suite drives both its refusal and its success — what
is being checked here is that no button offers it.
@@ -1727,7 +1802,7 @@ button that always refuses is the milder failure and is still worth removing.
### 12.8 Renaming a vault reaches every place its name is drawn · **needs two accounts**
Rename a shared vault from the VAULTS screen.
Rename a shared vault from Settings → Vaults.
**Pass:** the new name is on the vault list, on the badge of every host card in that vault, in the keychain
screen's "new items file to" picker, in the host editor's vault picker, and in the nav rail's user-chip
@@ -2149,10 +2224,11 @@ fifteen seconds comes from.
### 16.5 The version on screen is the version that was built
Right-click `DodoSSH.exe` → Properties → Details, and open PREFERENCES → UPDATES.
Right-click `DodoSSH.exe` → Properties → Details, and open Settings → General.
**Pass:** File version reads the tag (`0.1.0.0`), product **DodoSSH**, company **DodoTech**, and the
preferences screen prints the same number.
General page's UPDATES card prints the same number — the block moved there from Preferences in v5c, and
Preferences itself carries no version line any more.
**Failure means:** `0.0.0.0` is MinVer never seeing a tag — a shallow clone, or `fetch-depth` having been
dropped from a checkout. `1.0.0.0` is somebody having wired the app manifest's inert `assemblyIdentity`
@@ -2174,7 +2250,7 @@ script warns rather than failing when that is legitimate, which is the first rel
### 16.7 The update arrives, and the restart lands in it · **the whole point of the work**
With v0.1.0 installed and running, a vault unlocked, a host change made, and **a terminal open**, publish
v0.1.1 (`-Upload`). Then press CHECK NOW on PREFERENCES rather than waiting six hours.
v0.1.1 (`-Upload`). Then press CHECK NOW on Settings → General rather than waiting six hours.
**Pass:** the progress bar moves, the banner appears above the status bar, and — the part to actually watch
— the terminal **reflows cleanly rather than being sliced**, with the remote seeing the smaller row count.
@@ -2228,7 +2304,7 @@ there is one install directory, the pack ids collide and the nightly has replace
outright — which is the thing ADR 0013 decision 9 is constructed to make impossible, so it means one of the
four separations has been undone.
**And the direction that matters most:** on the release build, PREFERENCES → UPDATES → CHECK NOW must not
**And the direction that matters most:** on the release build, Settings → General → CHECK NOW must not
offer a nightly, ever, however many have been published since. It reads a different index and refuses
prereleases; if a nightly version is ever offered there, stop and treat it as a release-channel incident
rather than as a bug in the nightly.