Record v5 in the design-import log, and let the working notes go
ci / build and test (push) Successful in 2m15s
ci / android head (push) Successful in 3m28s
ci / desktop nightly (push) Successful in 44s
ci / api image (push) Successful in 24s

This commit is contained in:
2026-08-07 18:14:59 +02:00
parent 281e828e25
commit 7d64027972
3 changed files with 79 additions and 299 deletions
-168
View File
@@ -1,168 +0,0 @@
# Handoff: Hosts v5 design import — execution in progress
Written 2026-08-07, mid-execution, because the session hit its usage limit. A future session should
read this file plus `hosts-v5-design-spec.md` (same directory) and resume at "Where things stand".
## What this is
Implementing the Claude Design project screen `Hosts.dc.html`
(https://claude.ai/design/p/b20d580c-eae5-4cdd-bfae-88f669b5f8a3?file=Hosts.dc.html) in the desktop
Avalonia client. Worktree: `.claude/worktrees/connection-host-default-text-9e501d`, branch
`claude/hosts-design-implementation-22f161`. Nothing is committed yet — all progress is uncommitted
working-tree changes.
`hosts-v5-design-spec.md` holds the palette mapping table, the design's measurements, the monogram
hue pairs, and — critically — the **agreed deviations** from the mock (relay copy stays honest, dot
stays two-state, card click selects rather than connects, quick-connect keeps the auth word, find box
stays, "New group…" moves into the Group ▼ flyout, quick-access hint drops the "sidebar" claim).
## User-approved decisions (asked and answered, do not re-ask)
1. **Full re-theme** — shared palette swapped to purple/near-black; Montserrat + JetBrains Mono +
Material Icons embedded in the shared Shell project; Android recolors via the shared palette but
keeps its own default sans.
2. **Flat sections** — desktop hosts screen becomes one collapsible section per group (nesting
flattened phone-style, label order), "No group" first, Collapse All; group cards + breadcrumbs go.
Collapse state in-memory only.
3. **Pinned folders: full build**`HostSecret.PinnedPaths`, drawer QUICK ACCESS editor, pin-count
card badge, chip strip above the terminal; **clicking a pin opens the SFTP tab with the remote pane
at that path**.
4. **Last connected from the connection log**`max(StartedAt)` per `HostId` on hosts-screen
activation and on `SessionEnded`; relative time, restrung on a 1-minute tick; live sessions show
the green dot instead.
Execution method the user chose: **Sonnet subagents**, one wave at a time (waves 2+ serialize because
they all touch `src/DodoSSH.Client.Shell/ViewModels/VaultViewModel.cs`, ~11.8k lines, shared with the
Android head).
## Where things stand
### Wave 1 — DONE and verified (full `dotnet build DodoSSH.slnx`: 0 errors)
**Domain (complete, 264/264 tests pass in `tests/DodoSSH.Client.Domain.Tests`):**
- New `src/DodoSSH.Client.Domain/PinnedPathList.cs` — immutable, insertion-ordered, ordinal-deduped
(keeps first occurrence), `Empty`, `Create(...)`, `ToPathMap()` for the merge.
- `HostSecret.PinnedPaths` (default `Empty`), constants `MaxPinnedPathLength = 1024`,
`MaxPinnedPaths = 32`, validation wired into `TryValidate` (blank/control-char/oversize refused;
relative paths allowed).
- `HostSecretMerge.MergePinnedPaths` — per-element three-way merge; result order = base, then local
additions, then remote additions.
- `HostSecretCodec`: `PinnedPathsSchemaVersion = 7` (now current), field written last and null when
empty; old payloads decode as pinning nothing.
- UI usage: add = `host with { PinnedPaths = PinnedPathList.Create([.. host.PinnedPaths, p]) }`;
remove via `Create(host.PinnedPaths.Where(...))`.
**Theme (complete):**
- `src/DodoSSH.Client.Shell/Theme/Palette.axaml` fully re-colored per the spec table. New resources:
`AccentGradient` (#8D51E7#5D42DE), `AccentGlow` (BoxShadows `0 0 22 0 #598D51E7`), `IconFont`
(Material Icons, reference glyphs BY CODEPOINT, e.g. push_pin ``). `AccentInk` is now
white. `Live` #34D399. Essay comments updated where the recolor falsified them.
- Fonts embedded at `src/DodoSSH.Client.Shell/Assets/Fonts/` (Montserrat 4 statics, JetBrains Mono 3,
MaterialIcons-Regular; licenses alongside; `<AvaloniaResource Include="Assets/Fonts/**"/>` added to
the Shell csproj). `Program.cs` default sans → `avares://DodoSSH.Client.Shell/Assets/Fonts#Montserrat`
with Inter fallback; `MonoFont` → JetBrains Mono first. `tests/DodoSSH.Client.App.Layout.Tests/HeadlessApp.cs`
updated to keep its documented font parity with `Program.BuildAvaloniaApp`.
- `App.axaml`: tile/section radius 12, buttons/inputs radius 10, chips 6; `Button.accent` uses
gradient + glow (glow zeroed on :disabled). Deliberately skipped: raising global control MinHeight
to 40 (too risky unverified — style the hosts screen explicitly instead).
- Known stale doc: `docs/design-import-gaps.md:304` still says Inter/system-mono — wave 5 fixes docs.
### Wave 2 — WAS RUNNING when the session ended: state UNKNOWN, verify before building on it
A Sonnet agent was mid-flight restructuring the hosts screen. **Its changes may be complete, partial,
or absent in the working tree.** Before anything else: `git status` + `git diff --stat`, look at
`src/DodoSSH.Client.App/Views/HostsScreen.axaml` and `VaultViewModel.cs`, build, and run
`tests/DodoSSH.Client.App.Layout.Tests` + `tests/DodoSSH.Client.App.Tests`. If its work is partial or
broken, either finish it by hand or reset those files and re-dispatch.
Wave 2's full brief (re-usable as a subagent prompt):
- Flat sections per approved decision 2. Reuse/generalize the phone's flattening
(`SidebarRows` / `SidebarGroupHeader` / `ToggleGroup` / in-memory `collapsedGroups` ~line 1251).
No-groups keychain renders headerless (documented invariant). Section heading:
`{label} · {count}`, vault name beside it when >1 vault, per-section collapse, "Collapse All" on
the first heading row. Group Edit/Move/Delete on a heading menu (that order; no "Open"). Group
cards, `GroupTrail`, double-click-open, drag-host-onto-group-card all go (desktop only — phone
must not regress; it shares the VM).
- PRESERVE: multi-select (marquee `Band` + chosen-hosts ops + the three action panels), keyboard
nav, context menus, conflict panel, `NoVisibleHostsMessage`, find box (only search that crosses
group boundaries).
- Toolbar: "Hosts" title + count chip; Group ▼ / Tag ▼ checkable filter flyouts ("New group…" at the
Group flyout's foot); + New host accent button. Host passes tag filter if it wears ANY checked tag;
empty sections hide; filters ∩ find ∩ collapse compose.
- New card: 32×32 monogram avatar (two letters, stable-hash into the spec's four hue pairs, JetBrains
Mono bold), name 17 bold + two-state dot, mono `user@hostname` line (full address stays on
tooltip), pin badge (`IconFont` &#xE946; + `HostRowViewModel.PinCount` reading
`Host.PinnedPaths.Count`) only when >0, tag chips bottom-left, `LastConnectedText`
`[ObservableProperty]` slot bottom-right (EMPTY — wave 4 fills it), keep warn Badge/VaultBadge/✓/
pencil→`OpenHostPaneCommand`. Click selects, double-click connects. Responsive card widths.
- Update layout + behavioral tests; add coverage for flattening order, no-groups invariant, collapse,
filter composition, monogram stability.
### Wave 3 — NOT STARTED: drawer + pinned folders UI
- Restyle `src/DodoSSH.Client.App/Views/HostDrawer.axaml` to the mock: width 304→320, section labels
ADDRESS / GENERAL / CONNECTION / QUICK ACCESS (10px semibold, letterspacing 1.2), "Saving to
{vault} vault" inline picker (same gating: creating only + >1 writable vault), address field first
(mono, accent inset border), Name/group/tags/notes, username + 78px port on one row, auth
ComboBox, relay checkbox card — **KEEP the honest copy including "(not wired up yet)"
(HostDrawer.axaml:516-535); do not adopt the mock's relay text**. Footer: Save host
(accent/gradient, flex) + Cancel (92px ghost).
- QUICK ACCESS editor: rows (folder glyph &#xE2C7; via IconFont, mono path, ✕ remove), mono add
field + add button, hint sentence exactly: "Pinned folders appear above the terminal for this
host." Editor state on `VaultViewModel` following the `editorTagIds` pattern; enforce
`HostSecret.MaxPinnedPaths`/`MaxPinnedPathLength` before `TryValidate`.
- Detail pane (non-editing) should also show pinned paths read-only (design only shows the editor;
match the pane's existing fieldrow idiom).
- Terminal pin strip: chip row above the terminal for the connected host's pins, visible only when
the active tab's host has pins. Clicking opens the SFTP tab with the remote pane navigated to that
path for THAT host — needs an "open SFTP at path for host X" entry point on the transfers side
(extend what the context menu's "Browse files" already does; see `TransfersViewModel`). SFTP is a
second authenticated connection (documented, fine). Tab→host matching today is by label
(`MainWindowViewModel.RefreshConnectedHosts` ~3179) — reuse that convention and its documented
tradeoff.
- Android: the field syncs; phone editor UI deferred (document in wave 5).
### Wave 4 — NOT STARTED: last connected + quick connect
- Last connected per approved decision 4. Read via `VaultSession.ConnectionLog`
(`ConnectionLogSecret`: `HostId?`, `StartedAt`; entries written once at close). Compute on hosts
screen activation + on `TerminalWorkspace.SessionEnded`; fill `HostRowViewModel.LastConnectedText`
("just now", "N min ago", "N hr ago", "N days ago"); 1-minute restring tick while the screen is
visible; NO background log re-read (the log is deliberately not on the sync loop —
`LogsViewModel.cs:138-143`). Precedent: `MainWindowViewModel.RecentConnections` ~1113.
- Quick connect restyle (`src/DodoSSH.Client.App/Views/QuickConnect.axaml` + `MainWindowViewModel`
~1576-1730): card 520→~640 wide, radius 14, mono prompt `>_` styling, row = 8px live dot
(`IsConnected` exists on the row VMs' hosts) + mono name + mono address + auth word right
(credential/key/password — NOT the mock's SSH/SFTP constant). Selected row fills Accent with white
text. Footer hint kept.
### Wave 5 — NOT STARTED: docs + full verification
- New **v5 section** in `docs/design-import-gaps.md`, same format as v2/v3/v4: what shipped, then a
table of mock elements refused/deviated (amber dot; relay copy; card-as-link; SSH/SFTP kind column;
collapse persistence — in-memory, settings.json stays two scalars by decision; phone pinned-folder
editor deferred; Collapse All placement; "sidebar" claim dropped from the quick-access hint). Fix
the stale font row at ~line 304. Note the v3→v5 reversal (sections replace breadcrumbs) and the
v4 reversal (chips + address back on the card) explicitly, with the user's approval as the reason.
- Full `dotnet build DodoSSH.slnx` + ALL test projects. LF check over every touched file.
## Conventions that bit or will bite
- **LF everywhere** — one CRLF file fails the build via IDE0055; merges/agents reintroduce it.
Verify by raw bytes, not shell grep (two agents got false readings from shell quoting).
- Single test: `dotnet test <proj> -- --filter-method '<name>'` — plain `--filter` is silently ignored.
- Essay-style comments are load-bearing; never leave one asserting something a change made false.
- `PublicAPI.Unshipped.txt` analyzer exists only for `DodoSSH.Contracts` (not touched so far).
- Don't run two `dotnet build`s concurrently (shared artifacts dir) — serialize agent builds.
- The repo's honesty rule: nothing in the UI claims what the code doesn't do. This killed the mock's
relay copy, amber dot, and SSH/SFTP kind column — keep applying it.
## Suggested resume sequence
1. `git status` / `git diff --stat`; assess wave 2's actual state (complete/partial/absent).
2. Finish or re-dispatch wave 2; build + layout & app tests green before proceeding.
3. Waves 3 → 4 sequentially (both touch `VaultViewModel`), each verified by build + tests.
4. Wave 5 (docs + full sweep). Then review the whole diff, and commit in reviewable slices
(suggested: domain / theme+fonts / hosts screen / drawer+pins / ago+palette / docs).
5. Delete `HANDOFF-hosts-v5.md` and `hosts-v5-design-spec.md` before the branch is merged — they are
working notes, not repo docs.
+79 -1
View File
@@ -165,6 +165,84 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference
> | **Show more ⌄** | Not drawn as a disclosure. What it would hide — notes, the relay switch, forgetting the host key — is in the editor, one press away, and a second fold inside a pane that already scrolls is a second place for a field to be missing from. | > | **Show more ⌄** | Not drawn as a disclosure. What it would hide — notes, the relay switch, forgetting the host key — is in the editor, one press away, and a second fold inside a pane that already scrolls is a second place for a field to be missing from. |
> | **Port Forwarding** in the sidebar | Nothing, for the third time in this document. | > | **Port Forwarding** in the sidebar | Nothing, for the third time in this document. |
> | The host grid's toolbar avatar, share and tag-filter controls | Omitted, as in v3 and for the same reasons. | > | The host grid's toolbar avatar, share and tag-filter controls | Omitted, as in v3 and for the same reasons. |
>
> ## The desktop's v5 — hosts, restyled, and two reversals
>
> A fifth pass, and the first that undoes as much of its own predecessors as it adds: a re-theme both heads
> share, pinned folders as a full feature, a last-connected time read off the connection log, and two pieces
> of earlier passes put back on the user's own instruction rather than kept.
>
> **The palette went purple, and three fonts are embedded that were only ever named before.** The shared
> scheme moves from v3's near-black-and-blue to a purple accent (`#5D42DE`, with a gradient and a glow of its
> own) over the same near-black canvas, and both heads recolour together — `Palette.axaml` is shared, and a
> colour is not allowed to mean something different on the two of them. What changed on the desktop alone is
> the type: Montserrat, JetBrains Mono and Material Icons are embedded as `AvaloniaResource` in the Shell
> project rather than requested by name and left to whatever the machine happens to have, the way `WithInterFont`
> alone used to leave Inter registered but unused. Montserrat is the default sans now, with Inter kept as its
> fallback rather than the whole answer; `MonoFont` gains JetBrains Mono at the front of its own list, ahead of
> the system-mono stack that is still behind it for a glyph JetBrains Mono does not cover. Android recolours
> with the shared palette and keeps its own default sans — fonts are per-head by decision, only colour is
> shared.
>
> **Flat sections replace the grid of group cards and the breadcrumb trail — reversing v3, on the user's own
> approval rather than a defect found in it.** v3's grid held one level of the group tree at a time, opened by
> a double-click, with a trail above it saying where you were; that model is retired outright rather than
> folded into anything; every group is now a heading on one flat board, in label order, exactly the flattening
> the phone's `SidebarRows` has always drawn — a headerless board for a groupless keychain, "No group" first
> where there is one, a chevron per heading and one Collapse All. Nothing here was found broken; the grid did
> what v3 asked for. The user asked for the phone's shape on the desktop too, and decision 2 in
> `hosts-v5-design-spec.md` is that request recorded.
>
> **Tag chips and a mono address line are back on the card — reversing v4, also by approval and not by
> defect.** v4 had replaced both with a two-line `ssh, root, pci, eu-west-1` subtitle and moved the address to
> the tooltip alone, on the reasoning that a card read while scanning forty machines wants the kind of machine
> more than its address. v5's own mock draws chips and an address on every card, and the user chose the mock's
> arrangement over v4's when the two were put side by side — so the card now carries both: the full address is
> still on the tooltip, as v4 left it, and a truncated mono copy sits under the name as well.
>
> **Pinned folders are a full feature, not a field with nowhere to be edited.** `HostSecret.PinnedPaths` is an
> ordered, deduplicated list merged per path the way `TagIds` is merged per tag, so two machines pinning
> different folders on one host both keep theirs. The drawer's QUICK ACCESS section edits it — a row per pin
> with a close box, an add field and button, staged on `VaultViewModel.EditorPinnedPaths` the way the tag
> picker stages `editorTagIds` — the card shows a pin-count badge once there is at least one, and the detail
> pane repeats the list read-only in its own fieldrow idiom. Clicking a pin above a connected terminal opens
> the SFTP tab with the remote pane navigated straight to that path, through the same connect path "Browse
> files" already used — a second authenticated connection, exactly as every SFTP session here is.
>
> **Last connected is read off the synced connection log, and it is deliberately narrow about when.**
> `max(StartedAt)` per `HostId` is computed on the hosts screen's own activation and again when a session ends,
> then restrung into words on a one-minute tick for as long as the screen stays visible — never on the sync
> loop, which is the same rule `LogsViewModel` states for the log itself: nobody is watching their own
> connection from an hour ago update on a screen they are not looking at. A connected host prints nothing here;
> the green dot already answers "right now", and printing an age beside it would be answering a question about
> a session that is not the one still running. **It is scoped to the active vault alone**, the same limitation
> `LogsViewModel` and `MainWindowViewModel.RecentConnections` already carry — a host filed in a second vault
> this session can read gets no ago-text until something on this screen reads more than one vault's connection
> history, which nothing does yet. And a session ending can, briefly, still show the previous entry: the
> recorder's own write is queued onto a background task rather than made inline, so a re-read landing before
> that write drains reads the log as it stood before the session it is reporting on closed. It self-heals on
> the next activation or the next session end, and closing the gap outright was judged not worth blocking a
> screen transition on the recorder's queue.
>
> **Quick connect grew to match, and quietly answers a different question than the mock's own row does.** The
> card widened from 520 to 640, gained the mock's `>_` prompt styling and a live dot per row the way the hosts
> board's own two-state dot works. What it does not draw is the mock's SSH/SFTP kind column — every palette
> connection here is SSH, so a constant printed as though it varied would be exactly the kind of decoration
> this document has refused before (the terminal's session footer, the snippet's kind badge); the auth word
> (credential/key/password) answers a question that is actually different per row instead.
>
> | v5 element | What ships instead |
> | --- | --- |
> | The status dot's third, amber state | ◆ **Still two states, on the same reasoning v3 and v4 both gave and v5 was asked to reconsider.** Green means a terminal is open on that host, grey means one is not; nothing here pings a machine, so the amber "reachable but not connected" state has no fact behind it to draw. |
> | The relay checkbox's mock copy | The pre-existing sentence, unchanged, "(not wired up yet)" included — restyled into the mock's own nested-card shape (radius 12, a title beside the box rather than under it) but not reworded. The mock's own copy implies a relay this client can dial; it cannot, for the reason `HostDrawer.axaml`'s own remarks give. |
> | The host card as a link straight to a terminal | Click still selects, double-click still connects, and the pencil still opens the pane — the mock's card-as-link is not adopted, because multi-select (Ctrl, Shift, the marquee band) depends on a plain click meaning "choose this one" rather than "go". |
> | Quick connect's SSH/SFTP kind column | The auth word — credential, key, or password — see above. |
> | A collapsed section staying collapsed after a restart | In memory only, for the running session. `settings.json` holds two scalars by decision — the terminal's text size and whether this machine checks for updates on its own — and collapse state is not judged worth a third. |
> | QUICK ACCESS's editor, on the phone | **Deferred; the data is not.** `HostSecret.PinnedPaths` is shared, synced and merged on both heads, so a pin made on the desktop reaches the phone and back — Android just has nowhere yet to add or remove one itself. |
> | Collapse All beside every section's own collapse chevron | Bound on every heading's view model and shown on only the first — `SidebarGroupHeader.IsFirstBoardSection` is what a virtualised list of sections uses in place of a control of the board's own that would otherwise have to sit above all of them. |
> | The QUICK ACCESS hint's claim that pins live in a sidebar | "Pinned folders appear above the terminal for this host." — no sidebar exists on this screen for the sentence to point at, so the shipped hint says where they actually draw. |
> | The mock's "Saving to **DodoTech ▾** vault" subtitle, with a picker's chevron inside a sentence | The pre-existing `DrawerSubtitle` wording — the vault's name alone, unchanged by this pass. A chevron inside running text implies the text itself is the control, which it is not: the vault picker is its own element, shown only while creating and only above one writable vault, as it always has been. |
> | The design's two deeper text steps, `#6D6F84` and `#5D5F74` | Not added as `Palette.axaml` keys. `TextGhost`, already repicked to `#7C7F98` for v3, is reused everywhere the design reaches for either — a resource nothing yet distinguishes from `TextGhost` is not free to carry, on the same reasoning the border ramp's own remarks give. |
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
@@ -301,7 +379,7 @@ field cannot be removed and stays as a permanently refused member; `SyncEndpoint
| Status bar port forwards | client-ssh | Port forwarding. See below. | Omitted. | | Status bar port forwards | client-ssh | Port forwarding. See below. | Omitted. |
| Status bar `sftp · 2 transfers` | client-app | Nothing now — file transfer is built. What is missing is the count reaching the status bar, which is a screen away from where the queue lives. | Omitted from the status bar. The queue itself is on the FILES screen, with a row per transfer. | | Status bar `sftp · 2 transfers` | client-app | Nothing now — file transfer is built. What is missing is the count reaching the status bar, which is a screen away from where the queue lives. | Omitted from the status bar. The queue itself is on the FILES screen, with a row per transfer. |
| Status bar `locks in 09:41` | client-app | An idle auto-lock. See preferences below. | Omitted. | | Status bar `locks in 09:41` | client-app | An idle auto-lock. See preferences below. | Omitted. |
| IBM Plex Mono / IBM Plex Sans | ui | Shipping the font files as `AvaloniaResource` and registering them. The design loads them from Google Fonts, which a desktop app cannot. | Inter (already embedded) for prose, and the system monospace stack the terminal already names. Named once in `App.axaml` as `MonoFont`, so the substitution is reversible in one place. | | IBM Plex Mono / IBM Plex Sans | ui | Shipping the font files as `AvaloniaResource` and registering them. The design loads them from Google Fonts, which a desktop app cannot. | **Shipped, since v5, and not IBM Plex.** The v5 design asked for Montserrat, JetBrains Mono and Material Icons instead, and all three are embedded as `AvaloniaResource` in the Shell project rather than requested by name and left to whatever the machine happens to have. Montserrat is the default sans, with Inter — the earlier substitution — kept only as its fallback; `MonoFont` puts JetBrains Mono first, ahead of the system-monospace stack the terminal still falls back to. Named once each in `Program.cs` and `Palette.axaml`, so a later design's fonts are still a one-line swap. |
| `⌘K`, `⌥↵` | ui | Nothing; the design is Mac-flavoured. | `CTRL K`. Development is Windows-only today (`docs/platform-flags.md`). | | `⌘K`, `⌥↵` | ui | Nothing; the design is Mac-flavoured. | `CTRL K`. Development is Windows-only today (`docs/platform-flags.md`). |
**Also worth knowing:** custom chrome went in as `SystemDecorations="BorderOnly"`, not by extending the **Also worth knowing:** custom chrome went in as `SystemDecorations="BorderOnly"`, not by extending the
-130
View File
@@ -1,130 +0,0 @@
# Hosts v5 design import — spec and decisions
Source: Claude Design project `DodoSSH`, file `Hosts.dc.html` (1920x1080 desktop mock).
This file holds (a) the agreed decisions, (b) the palette mapping, (c) the raw design HTML at the bottom.
## Agreed decisions (user-approved)
1. **Full re-theme**: swap the shared palette (`src/DodoSSH.Client.Shell/Theme/Palette.axaml`) to the new
purple/near-black scheme — both heads recolor together. Embed Montserrat + JetBrains Mono +
Material Icons font files in the desktop head. Android keeps its default sans (fonts are per-head;
only colors are shared by decision); `MonoFont` gains JetBrains Mono at the front of its fallback list.
2. **Flat sections**: the desktop hosts screen becomes one collapsible section per group (nesting
flattened into one heading per group in label order, exactly like the phone's `SidebarRows`),
ungrouped hosts under "No group" first. Per-section collapse + Collapse All. Group cards and the
breadcrumb trail leave this screen. Group edit/move/delete move to a menu on the section heading.
Collapse state is in-memory only (settings.json stays two scalars by decision).
3. **Pinned folders (QUICK ACCESS)**: full feature. `HostSecret.PinnedPaths` (ordered strings,
per-path three-way merge like `TagIds`), drawer editor section, pin-count badge on cards, and a chip
strip above the terminal for the connected host; clicking a pin opens the SFTP tab with the remote
pane at that path (a second authenticated connection, as SFTP always is here).
4. **Last connected**: computed from the synced connection log (`max(StartedAt)` per `HostId`) on
hosts-screen activation and on `SessionEnded`; relative time ("just now" / "N min ago" / hours/days),
restrung on a 1-minute tick while visible, no background re-read.
## Deliberate deviations from the mock (do NOT implement the mock literally on these)
- **Relay checkbox copy keeps the honest wording** including "(not wired up yet)" — the design copy
implies a working relay; the client dial path does not exist yet. Keep the current sentence structure
in HostDrawer.axaml:516-535.
- **Status dot stays two states** (green = terminal open, grey = not). The design's amber has no
truthful meaning; nothing probes hosts.
- **Card click selects** (multi-select depends on it), double-click connects, pencil opens the detail
pane. The mock's card-as-link-to-terminal is not adopted.
- **Quick connect keeps the auth word** (credential/key/password) instead of the mock's SSH/SFTP kind
column — every palette connect is SSH, and a constant printed as a reading is refused in this repo.
- **Find box stays** (restyled) — it is the only search that crosses group boundaries.
- "New group…" moves to the foot of the Group ▼ filter flyout (the mock has no group-creation control).
- The QUICK ACCESS hint sentence ships as what is true: "Pinned folders appear above the terminal for
this host." (no "sidebar" claim).
## Palette mapping (design rgb → Palette.axaml key)
| Key | Old | New |
| --- | --- | --- |
| CanvasColor | #0E1220 | #05050A (design body rgb(5,5,10)) |
| Chrome | #111629 | #10111E (window rgb(16,17,30)) |
| Sidebar | #0F1426 | #0D0D16 (drawer rgb(13,13,22)) |
| Panel | #111629 | #10111E |
| Raised | #151B31 | #101019 (card rgb(16,16,25)) |
| Field | #0B0F1D | #101019 (inputs rgb(16,16,25)) |
| Chip | #16204A | #1A1A28 (rgb(26,26,40)) |
| Hover/Active/ChromeHover | — | derive slightly lighter than their surfaces, e.g. Hover #14151E, Active #1E1E2C, ChromeHover #1E1E2C |
| Border | #1A2138 | #1E1E2C (rgb(30,30,44)) |
| BorderMid | #232B45 | #24243A (rgb(36,36,58)) |
| BorderHover | #2E3859 | #2C2C3E (rgb(44,44,62)) |
| BorderSubtle/BorderFaint | — | keep one step darker than Border, e.g. #17172", use judgment |
| Text | #FFFFFF | #FFFFFF |
| TextDim | #B4BCD4 | #EDEDF5 (rgb(237,237,245)) |
| TextFaint | #7B849F | #9C9EB4 (rgb(156,158,180)) |
| TextGhost | #5A6280 | #7C7F98 (rgb(124,127,152)); the design also uses #6D6F84 and #5D5F74 for deeper steps — add keys only if needed |
| AccentColor | #5B8CFF | #5D42DE (rgb(93,66,222)) |
| AccentText | #8FA7FF | #A78BFA |
| AccentInk | #0E1220 | #FFFFFF (text on accent is white in this design) |
| AccentSoft/AccentWash | 0.35/0.06 | same opacities over #5D42DE |
| NEW: AccentGradient | — | LinearGradientBrush 180deg #8D51E7#5D42DE |
| NEW: AccentGlow | — | for BoxShadow use: 0 0 22 0 #8D51E7 at ~35% alpha |
| Live | #3DDC97 | #34D399 (rgb(52,211,153) — the design's own green) |
| Warn | #FFB454 | #F59E0B (rgb(245,158,11)); WarnText #F5B942 |
| Danger | #E86A6A | keep |
Radii: cards 12, controls 10, small chips 5-6. Field/control height 40. Primary buttons use
AccentGradient + glow.
Monogram avatar hue pairs (bg/fg), assigned by stable hash of host label:
violet #241C4F/#A78BFA · green #103A2F/#34D399 · amber #423211/#F5B942 · gray #1E1E2C/#9C9EB4.
## Raw design HTML (Hosts.dc.html)
The `<sc-for>`/`{{ }}` bits are the design tool's templating; `style-hover` is a hover style.
Sample data lives in the script block at the bottom.
```html
<div data-screen-label="Hosts" style="width:1920px;height:1080px;...background:rgb(16,17,30);...">
<dc-import name="TitleBar" on-search="{{ openQuickConnect }}" hint-size="100%,53px"></dc-import>
<div style="display:flex;flex-direction:row;flex-grow:1;min-height:0;">
<dc-import name="NavRail" active="hosts" mode="ssh" hint-size="255px,100%"></dc-import>
<div style="flex-grow:1;...padding:34px 36px;...">
<!-- Header row: title + count chip | Group ▼, Tag ▼, + New host -->
<span style="font-weight:700;font-size:33px;letter-spacing:-0.5px;color:#fff;">Hosts</span>
<div style="width:34px;height:30px;border-radius:9px;background:rgb(26,26,40);">10</div>
<!-- Group / Tag: h40, radius 10, inset 1px rgb(44,44,62) border, hover border accent -->
<!-- + New host: h40, radius 10, AccentGradient, glow, white 13.5 semibold -->
<!-- Section heading row (margin 32 0 16): "No group · 6" 12.5 medium rgb(124,127,152)
right side: collapse icon + "Collapse All" / "Collapse" 12 medium rgb(93,95,116) -->
<!-- Host grid: 3 columns, gap 14 -->
<!-- Host card: h120, radius 12, bg rgb(16,16,25), inset 1px rgb(30,30,44), hover border accent
- avatar 32x32 radius 9 at 21,20 with 2-letter mono bold 12.5 monogram
- name 17 bold white + 9px status dot, at 68,18
- addr mono 12.5 rgb(109,111,132) + pin badge (h19 radius 5 bg rgb(26,26,40),
push_pin icon 11 + count 10.5) at 68,39
- tag chips bottom-left (h21 radius 5 bg rgb(30,30,44), 11 medium rgb(156,158,180))
- ago text bottom-right 12 rgb(93,95,116) -->
</div>
<!-- Drawer (sc-if drawerOpen): w320, bg rgb(13,13,22), border-left rgb(26,26,40), padding 24 22
- "New host" 20 bold; subtitle "Saving to **DodoTech ▾** vault" 12.5
- section labels: 10px semibold letter-spacing 1.2 rgb(93,95,116): ADDRESS / GENERAL / CONNECTION / QUICK ACCESS
- ADDRESS: h42 radius 10 field with accent 1.5px inset border, ">_" prefix, mono value, blinking caret
- GENERAL: Name field h40; group dropdown ("No group" + expand_more); Add tag field + 40x40 add button; Notes h58
- CONNECTION: Username field + w78 port field ("22" mono) on one row; auth dropdown
("Password (ask each time)" + expand_more); relay checkbox card (radius 12, 18x18 checkbox,
title 13 semibold + body 11.5 rgb(124,127,152))
- QUICK ACCESS: per pin: h33 radius 9 row, folder icon + mono path 12.5 + close ✕;
add row: mono field "/path/to/folder" + 36x36 add button;
hint 11.5 rgb(93,95,116)
- footer: Save host (flex-grow, h44, AccentGradient + glow) + Cancel (w92 h44 ghost) -->
<!-- Quick connect (sc-if quickConnectOpen): full-window scrim rgba(5,5,10,0.6) + blur(3px),
card w640 radius 14 bg rgb(16,17,30) top-offset 160
- header row: ">_" accent mono + mono input "deploy@" + caret + "↵ connect · esc close"
- rows h44 radius 9: status dot 8px, name mono 13, addr mono 11.5, kind right
(selected row bg rgb(93,66,222), texts white) -->
</div>
</div>
```
Sample data (for tone, not to be shipped): hosts like `prod-api-01 / deploy@203.0.113.14 / pins 4 /
tags production,api / 2 min ago`; pinned = `/var/www/app`, `/etc/nginx`, `/var/log/pm2`.