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
+59 -2
View File
@@ -287,9 +287,9 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference
> | The tab and status dots' third, amber state | ◆ **Still two states**, for the reason the hosts screen's own dot has stayed two states since v3: green means a shell is open (or a session is connected), grey means it is not, and nothing here pings a host to justify a third colour meaning "reachable but not connected". | > | The tab and status dots' third, amber state | ◆ **Still two states**, for the reason the hosts screen's own dot has stayed two states since v3: green means a shell is open (or a session is connected), grey means it is not, and nothing here pings a host to justify a third colour meaning "reachable but not connected". |
> | The status bar's negotiated cipher, host-key algorithm and key/credential name | Omitted, and recorded rather than silently dropped. None of the three is reachable from this shell today: `SshNetConnection` holds the negotiated algorithms but nothing reads them off `ISshConnection`; `HostKeyPresentation` exists only for the trust questions a connection asks on the way in, not for a session already open; and a key's display name is never threaded past the connect call into anything the session keeps. What is real and bound is CONNECTED plus a dot, the session's own elapsed timer, and — terminal surface only — `UTF-8`, worded as a fact about this client's own renderer rather than a claim the remote agreed to. Candidates for future plumbing, not a defect closed here. | > | The status bar's negotiated cipher, host-key algorithm and key/credential name | Omitted, and recorded rather than silently dropped. None of the three is reachable from this shell today: `SshNetConnection` holds the negotiated algorithms but nothing reads them off `ISshConnection`; `HostKeyPresentation` exists only for the trust questions a connection asks on the way in, not for a session already open; and a key's display name is never threaded past the connect call into anything the session keeps. What is real and bound is CONNECTED plus a dot, the session's own elapsed timer, and — terminal surface only — `UTF-8`, worded as a fact about this client's own renderer rather than a claim the remote agreed to. Candidates for future plumbing, not a defect closed here. |
> | S3 dimmed in the design's own switcher | **Enabled.** The mock leaves S3 as future work; this application already has bucket browsing, so SSH, SFTP and S3 are a true three-way segment, wired to `IsSshShowing`, `IsTransfersShowing` and `IsBucketsShowing` exactly alike. | > | S3 dimmed in the design's own switcher | **Enabled.** The mock leaves S3 as future work; this application already has bucket browsing, so SSH, SFTP and S3 are a true three-way segment, wired to `IsSshShowing`, `IsTransfersShowing` and `IsBucketsShowing` exactly alike. |
> | The S3/Buckets screen | **Did not get the session shell this pass.** `TransfersScreen` serves both SFTP and S3 today and only the SFTP usage in `MainWindow.axaml` sits inside the new tab row/header/status bar/sidebar; the S3 usage is unchanged. | > | The S3/Buckets screen | **Did not get the session shell in v5b.** `TransfersScreen` serves both SFTP and S3 today and only the SFTP usage in `MainWindow.axaml` sat inside the new tab row/header/status bar/sidebar; the S3 usage was unchanged at the time. **v5c gives it the shell's own look without the machinery** — a 26-pixel padded, bordered, radius-12 container and nothing past that, since a bucket has no tab to close, no host to head a card with and no pin for a sidebar to show; see the v5c section, below. |
> | No pins destination in the design at all | **Kept anyway.** The rail still carries Pins — `KnownHostsScreen` — because the mock has no screen for approved host keys and this application's has to stay reachable. | > | No pins destination in the design at all | **Kept anyway.** The rail still carries Pins — `KnownHostsScreen` — because the mock has no screen for approved host keys and this application's has to stay reachable. |
> | The popover's Settings and Preferences rows, and the design's own Settings-* family of screens | Both land on the one screen this application actually has, `Preferences` — the design's own Settings area is future work, so this is two doors to one room rather than a door removed. | > | The popover's Settings and Preferences rows, and the design's own Settings-* family of screens | **Landed in v5c.** What was two doors to one room in v5b — Settings and Preferences both opening the same bare `Preferences` screen — is now two of three doors onto their own settings pages: Settings opens General, Preferences opens Preferences, and a third row, Vaults, opens Vaults. All three are real, distinct pages inside one settings mode; see the v5c section, below. |
> | `· Org` after the user chip's name, and a `Primary` tag on a vault row in the popover | Neither. There is no organisation concept behind a vault — only the vault itself — and no vault is distinguished as primary; the popover's vault rows are the existing shown-vaults toggles, restyled. | > | `· Org` after the user chip's name, and a `Primary` tag on a vault row in the popover | Neither. There is no organisation concept behind a vault — only the vault itself — and no vault is distinguished as primary; the popover's vault rows are the existing shown-vaults toggles, restyled. |
> | The design's titlebar, which has nowhere for a sync indicator | `SYNCED` stays, on the titlebar's right side, ahead of the window's own minimise/maximise/close buttons — the one thing this titlebar keeps that the design's own does not draw at all. | > | The design's titlebar, which has nowhere for a sync indicator | `SYNCED` stays, on the titlebar's right side, ahead of the window's own minimise/maximise/close buttons — the one thing this titlebar keeps that the design's own does not draw at all. |
> | Per-tab SFTP sessions, implied by a tab row shared between the terminal and the SFTP surface | **Not built, and not what shipped instead.** A click on the SFTP tab row runs `MainWindowViewModel.SelectFilesHostAsync`, which opens (or reuses) a second, SFTP-specific connection through the same "Browse files" plumbing a pin click already used — an honest second login, not a channel multiplexed onto the terminal's. `SelectedTab` moves with the click, which is also what keeps the sidebar's QUICK ACCESS in step — that list is keyed to `SelectedTab` on both surfaces, so selecting a different terminal tab afterwards can leave QUICK ACCESS naming a host that is not the one the remote pane is actually browsing. | > | Per-tab SFTP sessions, implied by a tab row shared between the terminal and the SFTP surface | **Not built, and not what shipped instead.** A click on the SFTP tab row runs `MainWindowViewModel.SelectFilesHostAsync`, which opens (or reuses) a second, SFTP-specific connection through the same "Browse files" plumbing a pin click already used — an honest second login, not a channel multiplexed onto the terminal's. `SelectedTab` moves with the click, which is also what keeps the sidebar's QUICK ACCESS in step — that list is keyed to `SelectedTab` on both surfaces, so selecting a different terminal tab afterwards can leave QUICK ACCESS naming a host that is not the one the remote pane is actually browsing. |
@@ -307,6 +307,63 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference
> | The design's shorter captions — "Type into terminal", the rationale-only sentence for "runs on insert" | The screen's own longer, more actionable wording kept instead: the insert button still names the destination tab (`TYPE INTO {tab}` / `NO TERMINAL OPEN`), and the "runs on insert" caption still states the operational consequence rather than only the reason the setting exists. Both of the design's sentences are true; the ones already here say more. | > | The design's shorter captions — "Type into terminal", the rationale-only sentence for "runs on insert" | The screen's own longer, more actionable wording kept instead: the insert button still names the destination tab (`TYPE INTO {tab}` / `NO TERMINAL OPEN`), and the "runs on insert" caption still states the operational consequence rather than only the reason the setting exists. Both of the design's sentences are true; the ones already here say more. |
> | A delete confirmation for a snippet | **Built, matching the mock.** `SnippetsViewModel.RequestDelete`/`ConfirmDelete`/`CancelDelete` say the same vault-wide-reach, tombstone, no-undo sentence `VaultViewModel.HowFarADeletionGoes` already says for every other item kind — additive beside the existing uncounted `DeleteCommand`, which the phone's own DELETE row still calls. | > | A delete confirmation for a snippet | **Built, matching the mock.** `SnippetsViewModel.RequestDelete`/`ConfirmDelete`/`CancelDelete` say the same vault-wide-reach, tombstone, no-undo sentence `VaultViewModel.HowFarADeletionGoes` already says for every other item kind — additive beside the existing uncounted `DeleteCommand`, which the phone's own DELETE row still calls. |
> | The Logs screen's own footer sentence about encryption | Adopted verbatim — verified against this screen's own header remark and ADR 0001 before shipping it, both true, so it is drawn as literal text rather than reworded. | > | The Logs screen's own footer sentence about encryption | Adopted verbatim — verified against this screen's own header remark and ADR 0001 before shipping it, both true, so it is drawn as literal text rather than reworded. |
>
> ## The desktop's v5c — a settings area, at last
>
> An eighth pass, and the first to give the design's own Settings area a home of its own rather than folding
> its two real screens into the rail's popover. Settings is a full-window **mode** now, swapped in wholesale
> rather than laid over anything: its own 53px titlebar reading "Back to application", a 340px `SettingsNav`
> rail — SETTINGS (General, Vaults, Account) above CUSTOMIZE (Security, Preferences, Groups, Tags), Logout
> pinned below both — and a content column capped at the design's own 1100 pixels, which is 741 beside the
> rail at the window's own minimum rather than the full 1100 (`LayoutHarness.SettingsContentWidth`). Seven
> pages, not the design's eight: the ORGANISATION section and its one page are refused outright, below. Two
> of the seven are wholly new — Account and Security did not exist as screens before this pass — and the
> other five are the old `PreferencesScreen.axaml` and `VaultsScreen.axaml` split apart and restyled into the
> design's own card idiom; both files are deleted, and every command either one offered is reachable here
> exactly once. The popover's own Settings, Vaults and Preferences rows, and everywhere else in the codebase
> that used to navigate to the bare `Preferences` or `Vaults` screen, now land in this mode instead —
> `MainWindowViewModel.ShowScreen` redirects at that one point rather than at every caller that used to reach
> either screen directly.
>
> The importer moved inside this same chrome rather than staying a screen of its own: it draws over the
> Preferences page as a boolean overlay — `SettingsNav` stays lit on Preferences the whole time it is open,
> and the titlebar's own back button reads "Back to preferences" instead of "Back to application" — restyled
> to the design's own table (ALIAS/HOSTNAME/USER/PORT/WHAT THIS MEANS, one header tick-all button in place of
> the old TICK ALL/TICK NONE pair). Known Hosts was restyled the same pass but kept its own place: it stays a
> main-chrome screen, reached from the rail's Keys entry and now also from Security's own "Approved host
> keys" row, because the design draws no page for it at all. And the S3/Buckets surface — which got none of
> v5b's session shell — picked up that shell's *look* this time without its machinery: a 26-pixel padded,
> bordered, radius-12 container and nothing else, since a bucket has no tab row, no host to head a card with,
> no status-bar fact to print and no pin for a sidebar to show.
>
> | v5c element | What ships instead |
> | --- | --- |
> | The Settings-Organisation page, and the rail's own ORGANISATION section | Refused outright. No organisation entity exists anywhere in this product — a team is the membership list behind a shared vault, and there is exactly one tenant per deployment — so the rail simply has no third section; see `SettingsNav.axaml`'s own remark. |
> | General: the update-channel switcher, Launch at login, Reopen tabs, the theme control and the language picker | Refused, and carried on the page's own NOT BUILT YET card rather than left silently missing: which channel a copy follows is fixed when it is built, nothing registers this application with Windows' own startup list or remembers a tab list across a launch, only the one dark theme exists, and there is no i18n anywhere in this client. |
> | Vaults: the VAULT DEFAULTS card (auto-lock, require-password-on-unlock, relay) and the RECOVERY card (kit, export) | Both refused outright — none of the three settings exists, sync is always on, and there is no recovery kit and no export. |
> | Vaults: "Manage devices", and the "3 devices" count on the sync line | Refused. There is no list-devices endpoint anywhere in this client. |
> | Vaults: per-vault UNLOCKED/LOCKED chips, and the "Unlock ⟨vault⟩" modal | Refused. This application locks the keychain as a whole, not one vault at a time, so there is no per-vault state for a chip or a modal to act on; the keychain-level fact is the sync card's own dot instead. |
> | Vaults: the magenta "Default" badge | Dropped rather than faked. The app does track a "new items go to" vault, but that lives on a different view model than the row being drawn here, and cross-referencing the two per card would be more machinery than the badge is worth. |
> | Vaults: "Sorted by name ▾" | Decorative in the mock, and not drawn — `VaultsViewModel.Vaults` is already ordered personal-first-then-name, and there is no second order to switch to. |
> | Vaults: member avatar stacks on every card | Refused on an unselected card — `VaultRowViewModel` carries a member *count*, not the members themselves, and only the selected vault's own member list is actually loaded. A count stands in on the card; avatars are real in the members panel, where the data is. |
> | Account: Edit profile, Change passphrase, the DEVICES list, Sign out everywhere | All four refused — no endpoint exists for any of them, and the passphrase is a one-time enrollment choice with no change flow. What is new and real on this page instead: the SIGN-IN card's issuer sentence, `MainWindowViewModel.Issuer` off `MeResponse.Issuer`, cached the same turn the name and email are and surfaced here for the first time — "Signing in proves who you are — it never decrypts a vault." |
> | Security: the strict-host-key toggle, the allowed-algorithms chips, the auto-lock link row | Refused — the app always asks on a changed key, there is no algorithm allow-list anywhere in the SSH stack, and there is no auto-lock setting for the link to point at. |
> | Security: RECENT SECURITY EVENTS as a list of rows | Shipped as the link alone, "See all activity in Logs." A real list would mean a filtered read over `LogsViewModel`'s two logs, and which entries count as "security" is a judgement call the design does not resolve; the honest link is complete on its own. |
> | Preferences: the device-name editor, Font/Cursor/Scrollback/Copy on select/Terminal bell, the clipboard-clear delay, confirm-run-on-insert | All refused, on the page's own carried-over NOT-BUILT idiom — none of the seven has anything behind it. Windows Hello's register/"Stop unlocking here" pair moved off this page to Security instead, one home rather than two. |
> | Groups: drag-to-reorder, and the mock's "the order here is the order there" sentence | Refused. `HostGroupRowViewModel` orders by the vault new items go into, then by vault name, then by label — there is no manual order to drag into. The page prints the true sentence instead and draws no drag handles. |
> | Tags: the LAST APPLIED column | Refused — no timestamp of when a tag was last put on a host exists anywhere in this client. |
> | Tags: a per-tag vault chip | Refused — `TagRowViewModel` carries a label and a host count and nothing else; there is no vault id on the row for a chip to read. |
> | Import: the "saving to ⟨vault⟩ ▾" picker | Refused as a control. Every import writes through the same `session.ActiveVaultId` every other bulk write does, so there is no per-import target to choose — the footer prints the vault's name as a fact, `ImportViewModel.SelectionSummary`, rather than as a `▾`. |
> | Import: the old ADDRESS/AUTHENTICATION/STATE columns | Folded into the design's own column set — ALIAS/HOSTNAME/USER/PORT — with the authentication text moved onto the alias cell's own tooltip rather than kept as a column of its own. |
> | Import: TICK ALL / TICK NONE | Replaced by the design's own header tick-all button, one press that ticks or unticks every row through `ImportRowViewModel.ToggleAllCommand`. |
> | Import as a nav-rail destination | Refused. It is a boolean overlay over the Preferences settings page (`MainWindowViewModel.IsImportOpen`), not a page of its own — `SettingsNav` stays lit on Preferences throughout, exactly as `Import.dc.html` draws it. |
> | The design's fixed 1100px content column | `MaxWidth="1100"` rather than a fixed width — the column is 741 pixels beside the 340px rail at the window's own minimum, where a fixed 1100 would not fit. |
> | The status bar and the update banner, while in settings mode | Both hidden — `MainWindow.axaml`'s bottom two rows read `!IsSettingsMode`. The design's own settings titlebar has no room for either, and there is nowhere honest to draw them instead. |
> | The design's sentence-case button copy | Not adopted. Every button on every settings page keeps this application's own ALL-CAPS mono convention — `CHECK NOW`, `SIGN OUT`, `OPEN IMPORTER` — over the mock's own "Check now". |
> | The design's assumption that Settings is the only thing on screen | Not followed. The quick-connect palette and an unapproved host key's decision card both still draw over settings mode exactly as they interrupt every other screen — a connection question does not stop mattering because the window happens to be showing Settings; see `MainWindow.axaml`'s own remark on the ordering of its Panel. |
> | The importer's own sentence, "This is the only control in DodoSSH that opens key material from a directory you did not point at file by file — nothing is read until Import is pressed." | Adopted verbatim — verified against `ImportViewModel.ScanAsync` and `ImportAsync`: scanning never calls `SshConfigLocator.ReadIdentity`, and `ImportAsync` is the only path that ever does. |
> | The tags page's own sentence, "Renaming here is one write and every host follows." | Adopted verbatim — verified against `SaveTagAsync`: a host names a tag by id, never by label, so a rename touches nothing but the tag item itself. |
> | The known-hosts screen's own intro, "Every pin is a decision recorded at the moment of connecting. Fingerprints are never trimmed — compare them character by character against what the operator published." | Adopted verbatim, matching the stance the rest of this application already takes on a pin. |
Most of it landed. This file is the rest: every element of that design with nothing behind it, which Most of it landed. This file is the rest: every element of that design with nothing behind it, which
project each piece would have to land in, and **what the shipped interface does instead**. That last project each piece would have to land in, and **what the shipped interface does instead**. That last
+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 `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. 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 ## 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 ### 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 **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. 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 **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. 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 **Pass:** every group is a heading on the board **at once** — in label order, "No group" first where it is
grid holds one level of it the way a directory pane holds one directory. What is left is the unfiled hosts. occupied — with its own hosts underneath. There is no double-click, no card to open a group into and no
**There is no heading and no fold on the desktop** — the headings, their chevrons and UNGROUPED are the breadcrumb trail: v3's one-level-at-a-time grid is gone outright, and every section is on screen from the
phone's, whose list has no room for a row of group cards and draws the whole tree flat instead. With every moment the board is. A host filed under `production` is drawn once, under that heading, and nowhere else on
host filed, the grid says so in a sentence rather than sitting empty. the board.
**Then press a group card once.** It is marked as chosen and **nothing else happens** — the grid is still the **Then press the chevron beside one heading.** It folds — the heading stays, with its live count, and its
level it was, and no buttons appear beside the GROUPS heading: editing and deleting a group are on the card's cards go — and pressing it again brings the cards back. **Then press "Collapse all"**, drawn on the first
own right-click menu, which is 7.9. **Then double-press it.** The group opens: its hosts heading only. It folds every section at once and its own label swaps to "Expand all"; opening any one heading
are the grid, the trail above the cards reads `ALL HOSTS <name> `, each card carrying the group's name as by hand swaps the label back to "Collapse all".
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 type a filed host's name into the find box at the top.** It is found from the outermost level, **Then type a filed host's name into the find box at the top, with its heading folded.** **Pass:** the
wherever it was filed and however deep, with the chip on its card saying which group it came out of. Clearing heading's own count narrows to match — the count is read off the set the find box has already narrowed,
the box puts it away again. Inside a group the same box reaches that group and everything under it, and the before the fold is looked at — but the cards themselves stay hidden until that heading is reopened; open it
empty-grid sentence there offers ALL HOSTS as the way to widen 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 — **Failure means:** a host drawn under a heading that is not its own, or under two at once, is
`SelectedItem` is `SelectedGroup`, and only `OpenGroupCommand` writes `GroupFilter`. Filed hosts still on the `VaultViewModel.AddFlatSection`'s membership test reading the wrong field for that section —
outermost level is `Matches` no longer comparing the host's group to the open one. A search that cannot find `host.Host.GroupId == group.EntityId` for a named heading, everything else for "No group". A chevron that
a filed host is worse than either: it is the box answering "no host matches that" about a machine the folds every section rather than the one it sits beside is `ToggleGroupCommand` being handed the board rather
keychain has got. A full-width bar with a chevron between the cards is the old grouping coming back through than the single `SidebarGroupHeader` it was pressed on. A count that does not move with the find box is
`SidebarRows`; the desktop grid binds `VisibleHosts`. See `HostsScreen.axaml`. `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 **Pass:** both groups are their own headings on the board, side by side — nesting is never drawn on the board
shown, with the trail reading `ALL HOSTS <outer> `. Double-press that, and the cards disappear entirely — itself, only carried in the group editor's own PARENT field. The inner heading's count is only the hosts
it has nothing inside it — while the trail stays. Pressing the **middle** crumb goes back one level rather filed directly under it, and the outer's is only the hosts filed directly under the outer one; the host filed
than all the way out, which is also how a group with nothing inside it is renamed: back out to the level under the inner group is not counted on the outer heading. Connect the host anyway: it dials the **outer**
where it has a card, and right-click that. 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 — **Then open the inner group's own editor and look at its PARENT picker.** The outer group is offered. **Then
the flat `Groups` is the phone's and the lookups'. A group that cannot be reached at all is worse and is the open the outer group's own editor and look at its PARENT picker.** The inner group is **not** offered — a
case `EffectiveParents` promotes: see 3.4a. 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 Scroll the board so one group's section fills the window, or fold every other section away, then press
the screen it was made on rather than somewhere the trail is not. The group editor likewise opens with that **+ New host**. Afterwards, open the Group ▾ flyout and press **New group…**.
group as its parent, so the new group is a card inside the one that is open.
**Failure means:** anything created inside a group disappearing the moment it is saved. That is the papercut **Pass:** the host editor opens with its GROUP picker on **"No group"**, and the group editor opens with its
a level-at-a-time grid comes with, and `NewHost` / `NewGroup` are where it is answered. Note the deliberate PARENT on **"No group"** too — neither reads anything from which heading happens to be on screen or scrolled
difference between them: the host editor also takes a merely *selected* card as its group, the group editor to. This is deliberate: the level-at-a-time grid these two commands used to inherit an "open group" from is
takes only the group that is open. 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 ### 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** ### 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. 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 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 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. 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 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 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. 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** ### 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 **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 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 ### 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 **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 **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` 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** ### 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 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 **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. — 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 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. 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 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 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. rather than as a bug in the nightly.