Commit Graph
413 Commits
Author SHA1 Message Date
jaap-jan fa85ad1eff Connections: drop the manual box's view-model state now that both heads ask through the prompt
ci / build and test (pull_request) Successful in 2m32s
ci / desktop nightly (pull_request) Skipped
ci / api image (pull_request) Successful in 7s
ci / deploy ssh.dodotech.cloud (pull_request) Skipped
ci / android head (pull_request) Successful in 3m41s
The phone's Connections screen no longer draws the user@host + password box, and the desktop never
did, so nothing bound VaultViewModel.ManualTarget, ManualPassword, ManualStatus or
ConnectManuallyCommand any more, nor MainWindowViewModel.ConnectToRecentCommand, whose address branch
wrote into that box. They are gone, and so is VaultViewModel.AskAboutHost, whose one caller was
ConnectToRecent. TryParseManualTarget, ManualEndpoint and ConnectToEndpointAsync stay: the prompt, the
desktop's Ctrl+K palette and the phone's hosts search all use them. The reasoning the removed members
carried moved onto ConnectToEndpointAsync and ChoosePromptRecentAsync.

The tests whose subject is still live now go through the prompt instead of the box: a typed address
dialled exactly as typed, port 22 by default, the refusal sentences, the host-key retry replaying the
attempt that raised it, the host-key card staying over the surface it was typed on, the session facts
for a typed password, and no forwards for a typed address. The recent-row cases now drive
ChoosePromptRecentCommand. Two cases existing prompt tests already covered were deleted. Manual check
11.8 now describes recent rows landing on the prompt.
2026-10-16 14:21:10 +02:00
jaap-jan 0cecf1afc4 Android: ask what to connect to on Connections, the same prompt the desktop's empty terminal draws
ci / build and test (push) Successful in 4m14s
ci / api image (push) Successful in 10s
ci / deploy ssh.dodotech.cloud (push) Successful in 22s
ci / android head (push) Successful in 5m6s
ci / desktop nightly (push) Successful in 3m2s
The phone's Connections screen with nothing open had a box that took an address alone, which left a
saved machine several screens away. It now draws the desktop's prompt over the same view-model state:
"connect to:" takes a saved host's name or user@host[:port], and a password line follows only when
one is needed. Names, addresses and refusals are read exactly as on the desktop, and nothing typed
here is saved.

What a phone changes: the hosts matching so far are thumb-sized buttons that take a host outright,
since there is no Tab; CONNECT is always drawn, since the IME's return key may only close the
keyboard; BACK, and the system back gesture, step off the password line rather than leaving the
screen; and the password's label sits on its own line. The keyboard follows the question onto the
password line, but is not raised when the screen opens, where it would cover the recent list.

Recent rows now go onto the prompt too. Covered by a new ShellFlowTests case for a tapped match on
top of the shared prompt cases; manual check 11.14.
2026-10-16 14:07:13 +02:00
jaap-jan 089de72834 Merge pull request 'Tests: wait for a dropped session's forwards to stop, not just for the tab to go dead' (#23) from claude/elastic-mccarthy-6b4ae7 into main
ci / android head (push) Successful in 3m50s
ci / desktop nightly (push) Canceled after 0s
ci / api image (push) Canceled after 0s
ci / deploy ssh.dodotech.cloud (push) Canceled after 0s
ci / build and test (push) Canceled after 4m25s
Reviewed-on: #23
2026-10-16 12:04:41 +00:00
jaap-jan f43436218a Tests: wait for a dropped session's forwards to stop, not just for the tab to go dead
ci / build and test (pull_request) Successful in 2m42s
ci / desktop nightly (pull_request) Skipped
ci / api image (pull_request) Successful in 7s
ci / deploy ssh.dodotech.cloud (pull_request) Skipped
ci / android head (pull_request) Successful in 3m42s
WhenTheSessionEnds_ItsForwardsSayTheyWentWithIt failed once under a full run. Ended clears IsLive
and then tells each forward its session is over, in one call. The app's dispatcher never splits that,
but the harness's post runs it on the fake shell's thread, so the test's poll on !IsLive could return
between the two and find the forwards still listening.

The test now also waits until every forward says "Stopped with the session.", the last thing
SessionEnded sets. A 300 ms sleep injected between the two steps fails the old test and passes the new
one; the full App.Tests suite passed three runs in a row.
2026-10-16 13:17:01 +02:00
jaap-jan 3c8c0980c2 Terminal: ask what to connect to when Terminal is pressed with nothing open
ci / build and test (push) Successful in 4m13s
ci / android head (push) Successful in 4m24s
ci / api image (push) Successful in 13s
ci / deploy ssh.dodotech.cloud (push) Failing after 1s
ci / desktop nightly (push) Successful in 3m6s
The terminal surface with no session used to be an empty frame, with the tab row's "+" the only
thing on it that did anything. It now holds a prompt, in the order ssh asks: "connect to:" takes a
saved host's name, matched exactly, with the hosts that match so far listed under it for Tab or a
click, or user@host[:port] for a machine that is not saved. A password line follows only when one
is needed, worded as ssh words it. Escape steps back a line; recent connections are listed below.

It is drawn by Avalonia in the WebView's rectangle, which the occlusion rule allows because the two
are exclusive: the prompt shows only while there are no tabs, the WebView only for a tab with a
session.

Saved hosts connect through a new VaultViewModel.ConnectToHostAsync, which the drawer's own connect
now goes through too, so every refusal is still made once. Each attempt now carries whether it may
act on the drawer's remember-password tick, and the prompt's may not: otherwise a tick left on in
a closed drawer would save a password typed at a prompt that never offered to keep anything.

Covered by nine ShellFlowTests cases and four ConnectPromptTests; manual check 7.10b.
2026-10-16 12:17:34 +02:00
jaap-jan 5b2e3f3651 Android: type user@host into the hosts search to open a terminal on a machine without saving it
ci / android head (push) Successful in 4m22s
ci / build and test (push) Successful in 4m39s
ci / api image (push) Successful in 7s
ci / deploy ssh.dodotech.cloud (push) Failing after 1s
ci / desktop nightly (push) Successful in 3m20s
The phone's only unsaved way in was the box on the terminal screen, which is drawn only while nothing
is open, so a second machine could not be reached that way while a first one's session was up. The
search well at the top of HOSTS is always there and the design already calls it "Search or
connect...", so it now reads an address the way the desktop palette does: with an @ in it, a NOT
SAVED card above the list offers a password box and CONNECT.

It connects through VaultViewModel.ConnectToEndpointAsync, the same path as the manual box and the
desktop palette, so nothing is written to the keychain. The search box and the password are cleared
once it dials, the password goes whenever the address changes, and an address the manual parser
refuses gets its sentence under the box.

Covered by six new ShellFlowTests cases; manual check 8.20 covers the card on a device.
2026-10-14 17:27:57 +02:00
jaap-jan 988248fa57 Quick connect: type user@host into Ctrl+K to open a terminal on a machine without saving it
ci / build and test (push) Successful in 5m48s
ci / api image (push) Successful in 47s
ci / deploy ssh.dodotech.cloud (push) Failing after 2s
ci / desktop nightly (push) Successful in 3m26s
ci / android head (push) Successful in 10m2s
The desktop had no way to reach a machine that was not in the keychain; the phone has had a manual
box on its terminal screen all along. The palette's query now reads as an address when it has an @
in it: a "not saved" row offers to dial it, Enter asks for the password in place, and a second Enter
connects. Escape on the password step goes back to the address.

It goes through the phone's own path, now split out as VaultViewModel.ConnectToEndpointAsync, so the
tab, the host-key question and the connection-log entry are the same on both heads and nothing is
written to the keychain. Password only, as on the phone: a key is the reason to save the host.

The typed row takes the highlight unless a saved host dials exactly that address, so typing a new
address cannot land on a saved machine whose address merely starts the same way.

Covered by eleven new QuickConnectTests; manual check 7.10a covers the real dial.
2026-10-14 17:02:26 +02:00
jaap-jan 902761d37b CI: deploy ssh.dodotech.cloud after the image is pushed, and fail unless it comes back ready
ci / build and test (push) Successful in 3m38s
ci / desktop nightly (push) Successful in 1m42s
ci / api image (push) Successful in 20s
ci / deploy ssh.dodotech.cloud (push) Failing after 0s
ci / android head (push) Successful in 6m3s
The image job pushed :main and stopped there, so nothing told the Spelunk
stack a new image existed and a commit carrying a migration never reached
the server's schema.

A deploy job now calls the stack's deploy hook with SPELUNK_DEPLOY_TOKEN and
then waits up to ten minutes for /healthz/ready. The new container applies
pending migrations as it starts, and readiness stays unhealthy while any is
pending, so a migration that did not apply fails the run instead of passing
quietly.

The Dockerfile no longer says nothing migrates from inside the container,
which had stopped being true and pointed at the setting that causes this.
2026-10-07 11:15:20 +02:00
jaap-jan 509937c4b8 Sync: keep the remembered sign-in when the provider is merely down, and say why SYNC NOW found the machine offline
ci / desktop nightly (push) Canceled after 0s
ci / api image (push) Canceled after 0s
ci / android head (push) Canceled after 4m10s
ci / build and test (push) Canceled after 4m12s
2026-10-07 11:11:09 +02:00
jaap-jan 5b32c76c95 Android: the redesign — a Vaults tab with hosts, keychain and snippets, card lists, a keychain that can add, and files that act on what is chosen
ci / build and test (push) Successful in 3m49s
ci / android head (push) Successful in 3m55s
ci / api image (push) Successful in 6s
ci / desktop nightly (push) Successful in 1m22s
The bottom bar is Vaults · Connections · Settings, drawn as Material 3 pills with icons from the embedded
Material font and a count of open sessions on Connections, which replaces the strip of shells above the
bar. Vaults is the open vault: its header carries the name as a switch to the vaults screen, the sync
light and a lock icon, and a Hosts · Keychain · Snippets segmented control sits under it. The keychain
and snippets move there from Settings (IsVaultSurface lights the tab; IsMoreSurface no longer lists
them), and back from either goes to the hosts. Settings becomes an account card and grouped sections:
security, vaults and history, files, this app.

Host rows are cards with a terminal tile and the live dot on its corner. The connect sheet names the
host and its address, labels its box, focuses it as it opens, reveals the password on request, and puts
CANCEL left and CONNECT right; every two-button row on the head now confirms on the right. While a
hosts-screen sheet is up the header and bar are dimmed too and a tap on them lowers it, and the bar
stands down while the software keyboard is up.

The keychain gains a floating + and a sheet of the desktop's five adds (generate a key, paste a key, a
password, a bucket, a tag) over the editors VaultViewModel already had, drawn as a full-screen page with
labels above the boxes; a chosen item gets EDIT and COPY PUBLIC KEY beside MOVE and DELETE. Snippets get
a floating + and cards that say RUNS or INSERTS. Files trade the seven-button block for a selection bar
(save, and a ⋮ sheet with rename, move, permissions and a red delete), folder and file tiles, an upload
button, and header actions for a terminal on the same host, refresh and close; a terminal's + sheet
opens SFTP on that terminal's own host.

The theme takes the audit's visual findings: Fluent's ClipToBounds is cleared on glowing buttons so the
glow is no longer cut to a square (the desktop's Button.accent too), prose is Montserrat with mono kept
for addresses and code, disabled secondary buttons are quieter than enabled ones, Fluent's dropdown,
checkbox, switch, focus and selection brushes come from the palette, touch no longer leaves hover fills
behind, and lists, nav entries and icon buttons carry names for TalkBack.

Builds clean; checked on the emulator end to end: unlock, the three vault views, adding and deleting a
password, connecting through the new sheet, files on the terminal's host with the selection bar and
its sheet. App tests 727 of 733 pass with 5 skipped; the one failure, TheCountsAPhoneShows_
FollowEveryForwardsState, passes three runs of three alone and is untouched here. Layout tests pass.
2026-10-04 15:50:09 +02:00
jaap-jan 6806e542f9 Terminal: measure characters with Unicode 11 widths, so an emoji takes the two columns the remote counts
ci / build and test (push) Successful in 3m37s
ci / android head (push) Successful in 3m45s
ci / api image (push) Successful in 8s
ci / desktop nightly (push) Successful in 1m44s
2026-10-04 13:55:20 +02:00
jaap-jan ad1a129159 CI: publish the desktop nightly with curl, since vpk's Gitea client cannot read the upgraded forge's replies
ci / build and test (push) Successful in 3m35s
ci / android head (push) Successful in 3m43s
ci / api image (push) Successful in 7s
ci / desktop nightly (push) Successful in 1m25s
Moving vpk to 1.2.161 was not enough: it got further and failed the same way, one frame later.
Both versions use a generated Gitea SDK that gets null back from 'create release' on the forge's
28.0.0, although the release is created, so the step threw before uploading anything and left an
empty draft behind on every push since 2 October.

The upload is now the android job's curl pattern. The release is created as a draft prerelease,
the three packages go up, then releases.win-nightly.json, which vpk pack already wrote and which is
exactly the index vpk upload would have generated, and only then is it published. The read-back
check, the RPM and the Linux manifest after it are unchanged.
2026-10-03 19:56:53 +02:00
jaap-jan e240c0cf26 Tests: prove the device-key probe runs off the caller's thread by blocking it, not by comparing thread ids
ci / build and test (push) Successful in 3m25s
ci / android head (push) Successful in 3m55s
ci / desktop nightly (push) Failing after 37s
ci / api image (push) Successful in 23s
TheChoiceIsMadeOnce_OffTheCallersThread failed on CI with the code doing the right thing. It
compared the probe's managed thread id with the test's, but the test runs on a pool thread, and that
same thread is free to pick up the queued probe as soon as the test awaits.

The probe now waits on a release the test only gives once construction has returned. A probe run on
the constructing thread could never be released, and the test fails after ten seconds instead of
passing by luck. Twenty runs pass; an inline probe fails it.
2026-10-03 19:50:25 +02:00
jaap-jan 96dd176121 CI: move vpk and Velopack to 1.2.161, whose Gitea upload survives the forge's new release responses
ci / build and test (push) Failing after 2m54s
ci / desktop nightly (push) Skipped
ci / api image (push) Skipped
ci / android head (push) Successful in 3m41s
The desktop nightly has failed on every push to main since 2 October, at 'publish the nightly
release'. vpk 1.2.0 creates the draft release and then reads release.Assets from the forge's
answer, which the upgraded server (28.0.0) no longer fills in for a new release, so it threw a
NullReferenceException at GiteaRepository.cs:136. The step deletes the previous nightly-desktop
release before it uploads, so desktop clients have had no nightly to update from since.

1.2.161 reads the assets null-safely. The library moves with the tool, since Velopack expects the
two to match. Every vpk option the workflow and the release scripts pass is present in 1.2.161;
the only renamed ones are two MSI options nothing here uses.
2026-10-03 19:44:25 +02:00
jaap-jan 367cebae9b Android: a customisable key bar, a quick-access keyboard, a long-press cursor gesture, and a side panel on tablets
ci / android head (push) Successful in 5m59s
ci / api image (push) Successful in 8s
ci / build and test (push) Successful in 4m52s
ci / desktop nightly (push) Failing after 58s
The key bar above the software keyboard is now the user's own: which keys, in what order, up to sixteen
in two rows that never scroll. Ctrl and Alt latch across the bar and the phone's keyboard, and Ctrl or Alt
with a cursor key sends the modified CSI form. Seven predefined groups (essentials, cursor, control,
symbols, tmux, vim, function) and keys of one's own, written in \e / ^C notation, are arranged on a
customise sheet that saves as it goes.

A grid button swaps the software keyboard for a quick-access keyboard of the same height, with KEYS,
SNIPPETS, HISTORY and APPEARANCE tabs; it reopens to the tab last used. Snippets go through the shell's
existing InsertSnippet. History keeps a typed command only once the remote has echoed it, so nothing
typed at a password prompt is ever recorded; it is memory-only and LOCK clears it. Each session pump now
keeps its last kilobyte of output and the alternate-screen state for that test (OutputTail).

Long-press and drag on the terminal sends arrow keys, as a joystick with three speeds by distance;
scrolling is unchanged and there is no mode switch. It takes the touch before Chromium's own long press,
so a held finger no longer pastes; paste is on the bar by default instead. A switch in APPEARANCE turns
the gesture off. The text-size buttons moved from the bar to APPEARANCE.

On wide screens the same tabs open as a side panel beside the terminal, toggled from the top bar, with
the bar's own keys repeated at the top of KEYS while the bar stands down.

The model, gesture arithmetic, echo reading and history are in DodoSSH.Client.Shell and covered by
TerminalKeyboardTests and OutputTailTests. Builds clean; not yet exercised on a device, because the
emulator's install is locked behind a passphrase that was not recorded.
2026-10-03 14:29:22 +02:00
jaap-jan c21de4ade4 Docs: the 2026-10-02 audit reports, the full sweep and the Android design audit, with their screenshots
ci / build and test (push) Successful in 4m49s
ci / android head (push) Successful in 5m26s
ci / api image (push) Successful in 9s
ci / desktop nightly (push) Failing after 56s
REPORT.md is the full-app sweep and what became of each finding. ANDROID-DESIGN.md is a walk through
every phone screen on the emulator, from a cleared install to SFTP, with findings ranked by severity
and a screenshot for each. The home-folder listing in the sweep's transfers screenshot is blurred.
2026-10-03 11:43:03 +02:00
jaap-jan 858e36279d Sync: after a read from the beginning, remove what the server no longer has, and flag an edit that brings a forgotten item back
ci / build and test (push) Successful in 3m38s
ci / android head (push) Successful in 3m46s
ci / desktop nightly (push) Failing after 37s
ci / api image (push) Successful in 48s
The server now forgets a deletion past its retention and refuses cursors
from before it. The client then reads everything again, but everything
again says nothing about what went. So a machine away for longer than the
retention kept those items for good, and its next edit of one put it back
on the server for everyone.

A read from the beginning now marks each item it reads, keeping the mark in
the sync state because the read can span passes. When it finishes, it
removes the items of the synced types it never marked. Two kinds are kept:
items newer than the read's bound, which this machine pushed during it, and
items with a change still waiting, which are the user's work. For those,
the push finds the server without the item and sends it as a create, as
before, but now records a RecreatedAfterCollection conflict and counts it,
so the sync message says so instead of undoing someone's deletion silently.

The bound is the highest sequence the cache held when the read began, or
anything higher it has read since. Taking only what the read returned would
have left the last item of a vault in place for ever.
2026-10-02 17:36:11 +02:00
jaap-jan 55e5de0ce4 Server: a cursor carries the sync horizon it was issued under, so a read from the beginning after a collection is not refused on its own pages
A cursor behind the horizon was refused on position alone. But the read a
refused client starts from the beginning is itself behind the horizon on
every page until it passes the collected deletion, since older untouched
items come first. So its second page was refused, it started over, and it
never finished, on any vault larger than one page.

The cursor now records the horizon read before the page that produced it,
and only a cursor issued under an older horizon is refused. A cursor issued
under no horizon keeps the v1 format, so a server from before this still
reads it.
2026-10-02 17:36:03 +02:00
jaap-jan 8cb1457ca9 Server: collect tombstones past their retention, compact superseded change entries, and refuse cursors from before a collection 2026-10-02 17:18:22 +02:00
jaap-jan 656a33b4b1 Desktop: build only the host cards near the view, with a virtualising wrap panel for the board
ci / build and test (push) Successful in 3m39s
ci / android head (push) Successful in 3m59s
ci / api image (push) Successful in 5s
ci / desktop nightly (push) Failing after 37s
2026-10-02 16:20:51 +02:00
jaap-jan 41f368ae75 Merge remote-tracking branch 'origin/main' into claude/audit-followups
ci / build and test (push) Successful in 3m45s
ci / android head (push) Successful in 4m15s
ci / desktop nightly (push) Failing after 43s
ci / api image (push) Successful in 46s
2026-10-02 15:27:39 +02:00
jaap-jan 24c18205c3 Desktop: rebuild the hosts board once the typing in the find box pauses, not once a letter 2026-10-02 15:23:58 +02:00
jaap-jan 8759b12d47 Desktop: virtualise the transfer queue and the import lists 2026-10-02 15:14:34 +02:00
jaap-jan 7b072bcded Desktop: swap the host lists in whole instead of announcing every row to listeners that redo all their work per row 2026-10-02 15:07:08 +02:00
jaap-jan b1d4a270d4 Merge pull request 'Audit follow-ups: keyboard, wording, performance, transfer, sync, retry and relay fixes' (#22) from claude/audit-followups into main
ci / build and test (push) Failing after 3m10s
ci / desktop nightly (push) Skipped
ci / api image (push) Skipped
ci / android head (push) Successful in 4m7s
Reviewed-on: #22
2026-10-02 12:59:55 +00:00
jaap-jan a2362d1e3e Desktop: decide a titlebar double-click on the press, before the drag can swallow it
ci / build and test (pull_request) Successful in 2m48s
ci / desktop nightly (pull_request) Skipped
ci / api image (pull_request) Successful in 13s
ci / android head (pull_request) Successful in 3m41s
2026-10-02 14:40:50 +02:00
jaap-jan 079a273d46 Desktop: let a lock pressed during an unlock win, instead of the unlock finishing behind it 2026-10-02 14:37:52 +02:00
jaap-jan 37e9d74938 Desktop: keep a vault-visibility failure from closing the app, and leave a crash log when anything else does 2026-10-02 14:28:27 +02:00
jaap-jan 170caa1391 Relay: end a user's sessions into a vault the moment they lose access to it 2026-10-02 14:26:44 +02:00
jaap-jan 965c9a7043 Server: stop a team being archived while a vault is created in it 2026-10-02 14:20:30 +02:00
jaap-jan ff8a790f6c Sync: take the server having no such item as a delete done, not a rejection nobody can clear 2026-10-02 14:18:28 +02:00
jaap-jan b2516ca7fb Server: start a retried write transaction from the database, not from the attempt that failed 2026-10-02 14:15:52 +02:00
jaap-jan 36fb0817a4 Transfers: say so when a drop or a download is refused because the screen is busy 2026-10-02 14:11:41 +02:00
jaap-jan cc3d46f78c Transfers: let cancel pressed as a transfer finishes do nothing rather than throw 2026-10-02 14:10:40 +02:00
jaap-jan 8bee476039 Transfers: stop a transfer queued as the last one finishes from waiting for ever 2026-10-02 14:09:47 +02:00
jaap-jan 441709f1e9 Desktop: stop decrypting the whole keychain on a sync pass that changed nothing 2026-10-02 14:08:08 +02:00
jaap-jan 873405e591 Sync: mirror a pulled page in one write and read the outbox once per page, not twice per change 2026-10-02 14:04:36 +02:00
jaap-jan f997e87c5f SSH: open a shell off the caller's thread, and stop waiting when the caller gives up 2026-10-02 14:02:18 +02:00
jaap-jan 365954c541 Desktop: probe for the TPM once, off the UI thread, instead of on every unlock 2026-10-02 14:00:12 +02:00
jaap-jan 3ccafdd53b Desktop: drop the VAULT columns that had nothing in them, and the snippet buttons with nothing to act on 2026-10-02 13:58:07 +02:00
jaap-jan 22b4d82ba8 Desktop: stop the status bar's session timer when the shell ends 2026-10-02 13:54:24 +02:00
jaap-jan 619f49a35a Desktop: trim a long session tab label instead of letting it push the + off the strip 2026-10-02 13:54:24 +02:00
jaap-jan a6d3924e0d Desktop: call device unlock what this platform calls it, not Windows Hello everywhere 2026-10-02 13:52:00 +02:00
jaap-jan 079914bfb4 Desktop: let the arrows choose the host the drawer acts on, and Enter connect to it 2026-10-02 13:49:36 +02:00
jaap-jan 6c09bdecb6 Desktop: give the host-key, delete and sign-out questions the keyboard on their harmless answer, and let Escape say no 2026-10-02 13:45:40 +02:00
jaap-jan 372a7cbdc7 Desktop: put the keyboard in the closed vault's card on launch, and let Enter sign in and enrol 2026-10-02 13:39:12 +02:00
jaap-jan a4b81da597 Desktop: show the right vault's people, leave settings on lock, say when an import fell short, and stop host names being cut to four letters
ci / build and test (push) Successful in 3m26s
ci / android head (push) Successful in 3m50s
ci / desktop nightly (push) Failing after 37s
ci / api image (push) Successful in 17s
- Choosing a vault while the screen was busy went through a gate that did nothing, so the members
  panel kept showing the previous vault's people under the new name. The read now starts regardless;
  the selection generation discards answers for an earlier choice.
- Locking or signing out from Settings left the window in settings mode under the lock card, with no
  title bar to drag, minimise or close, and unlock returned to Settings. Leaving the unlocked state
  leaves Settings and closes the members panel; Escape closes that panel before leaving Settings.
- Both importers reported success when the keychain was busy and skipped the batch, or failed partway.
  ImportOutcome now carries refused hosts and why a batch stopped, and the screens say so.
- Host cards: the sync badge took 70 of 214 pixels from the name ("bastion" drew as "bast…", two k8s
  workers both as "k8s…"); it now sits on the address line. Tag chips wrap and trim inside the card.
  Keychain names trim inside their column instead of printing over TYPE, and the Security page's
  caption wraps inside its card.
2026-10-02 13:16:55 +02:00
jaap-jan 0e8fe4fc80 Give each profile its own device key, and make the Linux PIN window able to open at all
- The Windows CNG key and the macOS keychain tag were one name for every profile, so signing out of
  the nightly deleted the key the release build's device unlock was sealed to. DeviceKeyNames keeps the
  existing name for the default profile and gives every other profile its own; sign-out deletes only
  its own key, and a profile sealed before this keeps reading the shared one.
- DevicePinWindow named two palette resources that do not exist and loaded itself with
  AvaloniaXamlLoader.Load, which never assigns the named fields, so it threw on construction and no
  PIN prompt was ever shown. A layout test now builds it against the real palette.
2026-10-02 13:16:48 +02:00
jaap-jan 2ff9dff560 Terminal: deliver large pastes whole, keep keystrokes in order, and keep one session's failure off the shared socket
- A message larger than the 64 KiB receive buffer was skipped and its tail dispatched as a frame of its
  own. Messages are now reassembled up to 16 MiB; one beyond that is dropped whole.
- Input frames are handed to the pump without waiting, and ShellStream takes no lock around its write
  buffer, so a snippet's text and its carriage return could interleave or lose bytes. Writes are
  serialised per pump in arrival order.
- A resize reaching a shell SSH.NET had already disposed threw out of the receive loop and closed the
  socket every tab shares. Each frame's failure now stays with its session.
2026-10-02 13:16:48 +02:00
jaap-jan 6846f67c20 Server: send each item at the revision its ciphertext is sealed at, keep team archiving with the owner, refuse suspended accounts, and stop enrolment failing after an hour
- Pull hydrated every historical change entry with the item's current ciphertext under that entry's
  old revision. The AAD binds the revision, so a client opened it at the wrong version, failed, and
  parked its own pending edit as undecryptable. Entries an item has since moved past are left out; the
  later entry carries the same payload under the right number, and a fresh device no longer downloads
  an item once per edit it ever had.
- Deleting a team's last vault archived the team whoever deleted it, so an admin could archive it out
  from under its owner. Only the owner's delete archives it now, and TeamService.ArchiveAsync checks.
- UserStatus was checked nowhere on the request path. ActiveAccountRequirement on both policies answers
  403 account-not-active for a suspended or deprovisioned account.
- The hourly last-seen update moves xmin under the tracked user, and enrolment then saved that user
  against the stale token: a 500 for anybody enrolling more than an hour after first signing in. The
  entity is reloaded after the update.
2026-10-02 13:16:48 +02:00