Public Access
Merge main into the teams branch
Two conflicts, and both were two people counting the same things differently rather than disagreeing about what the code should do. PhoneShell's header comment. The branch made "the five hub screens" numberless, because TEAMS made it six and a number in that sentence had already gone stale once. Main corrected "three destinations" to "two" in the same sentence, because giving a shell the whole phone took the terminal out of the set the header is drawn on. Both are right and neither noticed the other: the header now stays on the hub's screens and on the two top-level destinations, which is Hosts and Keychain. The manual checks. Both sides appended a Phase 10 — main added the software keyboard and the phone's terminal surface as 10 and 11, the branch added Teams. Nothing about them overlaps, so the resolution is to keep all three in the order they were written and renumber Teams to Phase 12, its subsections and the one cross-reference inside 12.1 with it. Main's two phases keep the numbers they already carry in its history, since renumbering those would move headings somebody may already have linked to. Everything else merged without a conflict, and the two places worth checking afterwards both held: IsMoreSurface and the first case of PhoneShell.OnBackRequested each kept ShellScreen.Team alongside main's edits. Those two are one fact in two places, so a merge that dropped Team from either would have trapped the user on the teams screen with the MORE tab dark. Verified after resolving: solution builds with no errors and no new warnings, the Android head builds, and every suite passes — App 214, Layout 73, Api 162, Infrastructure 34, Contracts 25, Session 54. App gained the three shell-flow tests main brought with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -509,6 +509,14 @@ go at 360dp:
|
||||
drawn, because a modifier that is on and does not look on is how somebody sends `^L` to a database prompt
|
||||
believing they typed an `l`.
|
||||
|
||||
**The surface has since taken the whole screen.** `PhoneShell` collapses the header, the session strip
|
||||
and the bottom bar while a terminal is showing — one binding on `IsShowingPages` each — and the screen
|
||||
draws a 52-pixel bar in their place: back, the session pills, and a `+` raising a sheet with the three
|
||||
connections there are. That sheet is the head's first control that could be drawn over the renderer, so
|
||||
it collapses it rather than covering it, exactly as the desktop's palette does; whether Android's
|
||||
WebView actually composites above Avalonia content is still the unverified question recorded below, and
|
||||
collapsing is correct under either answer.
|
||||
|
||||
All eight are done for the decided scope, and v2 has since gone past it — see the note under step 7.
|
||||
|
||||
What is left, in the order it matters:
|
||||
@@ -564,3 +572,16 @@ Recorded so they are choices rather than accidents. Any of them is cheap to revi
|
||||
- **`NativeKeyboardFocus` is not ported.** It exists for a documented Win32 asymmetry — focus crosses into
|
||||
WebView2 but does not come back — and Android's focus model is different enough that the problem should be
|
||||
confirmed to exist before anything is written to solve it.
|
||||
- **The software keyboard is kept off the interface in one place, and by two mechanisms.** `PhoneShell`
|
||||
owns it rather than each screen, because everything the phone draws is inside that one control and a
|
||||
screen added later would otherwise have to remember. The two mechanisms are not a belt and braces: before
|
||||
Android 15 the activity's `AdjustResize` has the platform shorten the window and the reported keyboard
|
||||
inset arrives already consumed, and from Android 15 edge-to-edge is enforced, the window is no longer
|
||||
resized for the keyboard, and the inset is what there is. Each is dead where the other applies, which is
|
||||
why the margin is taken from the inset alone — the two added together would strand the interface an
|
||||
entire keyboard too high. See `docs/manual-checks.md` phase 10, which is the only way either is verified.
|
||||
- **A box that takes a secret says so twice.** `PasswordChar` is what the screen draws and
|
||||
`TextInputOptions.ContentType` is what the software keyboard is told, and only the second one turns off
|
||||
the suggestion strip and keeps the passphrase out of the IME's learning dictionary. The desktop head
|
||||
needs only the first, which is why the phone's `TextBox.secret` class carries both rather than the two
|
||||
being set per box.
|
||||
|
||||
@@ -31,7 +31,7 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference
|
||||
> | v2 element | What ships instead |
|
||||
> | --- | --- |
|
||||
> | The **FORWARDING** screen: local/remote/dynamic rules, toggles, bytes transferred | **Nothing, said out loud.** `ISshConnection` offers `OpenShellAsync` and nothing else, so there is no tunnel for a rule to run through; `SyncEntityType.PortForward = 9` is still reserved and still unused. The MORE screen carries a paragraph naming the absence, for the reason the desktop keeps TEAMS in its rail. |
|
||||
> | `23 ms · fwd 5432` on the terminal's connection line | The account and endpoint actually dialled. There is no RTT measurement in SSH.NET and nothing forwards anything. |
|
||||
> | `23 ms · fwd 5432` on the terminal's connection line | ◆ **The line is gone, and what was real on it moved.** There was never an RTT to draw — SSH.NET measures none — and nothing forwards anything, so what shipped was the account and endpoint actually dialled. In v3 a connected phone draws one 52-pixel bar and then the terminal, so a second 36-pixel row naming the machine is exactly the chrome that surface exists to give back: the address is on the connecting card, where it is read before anything has answered, and the shell's own prompt says it afterwards. The two text-size buttons that shared the line are pinned at the end of the accessory row, outside its scroller, which is what the line was protecting them from. |
|
||||
> | `ED25519` badge and `SHA256:kQ9f…Zw2M` on every keychain card | `Detail`, which is what is genuinely known *about* an item. Unchanged from the first import: no algorithm field, no fingerprint, and computing either means parsing armour the type stores verbatim. |
|
||||
> | An `agent` chip on a key | Omitted. There is no agent of any kind — see the first import's Vault section. |
|
||||
> | Snippet cards footed with `edge-eu-1 · today 10:58 · exit 0` | The command and its badge. Nothing records where a snippet ran, when, or what it returned; the shell it is typed into never reports back. |
|
||||
|
||||
+131
-11
@@ -835,7 +835,127 @@ On the phone's host editor with several tags: the chips are at least 36 tall, sp
|
||||
lands between them rather than on the wrong tag, and the new-tag box and ADD sit on one row without either
|
||||
being squeezed to nothing.
|
||||
|
||||
## Phase 10 — Teams: the operations that span two accounts
|
||||
---
|
||||
|
||||
## Phase 10 — The software keyboard and the boxes that take secrets
|
||||
|
||||
Every check here needs a real Android device or emulator, and there is no headless equivalent of any of
|
||||
them: the software keyboard is an inset the platform reports, and a headless top level reports none.
|
||||
Worth running on two devices if you have them — one on Android 14 or earlier and one on Android 15 or
|
||||
later — because the interface is kept clear of the keyboard by a different mechanism on each. Before 15 the
|
||||
activity's `AdjustResize` has the platform shorten the window; from 15 the window is not resized at all and
|
||||
`PhoneShell` applies the reported inset itself. A build that only ever ran on one of the two will look
|
||||
correct and be half broken.
|
||||
|
||||
### 10.1 The vault passphrase box is treated as a password by the keyboard
|
||||
|
||||
Launch to the lock screen, tap the passphrase box, type a few characters.
|
||||
|
||||
**Pass:** dots on screen, and **no suggestion strip above the keyboard** — no completions, no previously
|
||||
typed words, no autocorrect. Then open any ordinary box on the phone (the host search, a snippet's name)
|
||||
and confirm the suggestions come back there.
|
||||
|
||||
**Failure means:** `TextInputOptions.ContentType` is missing — most likely a box was given `PasswordChar`
|
||||
directly instead of `Classes="... secret"`. `PasswordChar` is what the screen draws; the content type is
|
||||
what the keyboard is told, and only the second one keeps a passphrase out of the IME's learning
|
||||
dictionary. A box showing dots with a suggestion strip over it is the worst case, not a cosmetic one.
|
||||
|
||||
### 10.2 The keyboard does not cover the box being typed into
|
||||
|
||||
The same box: with the keyboard up, the passphrase box and the UNLOCK button under it are both visible.
|
||||
Repeat on each of the five boxes that take a secret — lock screen, both enrollment boxes, the connect
|
||||
password on HOSTS, and the connect password on FILES.
|
||||
|
||||
**Pass:** the box stays on screen when the keyboard opens, and the interface is shortened rather than slid
|
||||
— the header stays where it is rather than scrolling off the top.
|
||||
|
||||
**Failure means:** on Android 15 or later, the inset is no longer reaching `PhoneShell`. On 14 or earlier,
|
||||
`WindowSoftInputMode` has been dropped from the activity and the platform is panning the window instead of
|
||||
resizing it — which, for a window whose whole content is one native view, pans by nothing useful.
|
||||
|
||||
### 10.3 Nothing is stranded when the keyboard closes
|
||||
|
||||
Dismiss the keyboard with back or the down-chevron from each of those screens.
|
||||
|
||||
**Pass:** the interface fills the screen again immediately, with no band of empty canvas left along the
|
||||
bottom and no scroll position left part way down.
|
||||
|
||||
**Failure means:** the inset is being applied but not cleared — the closed state is not being read from the
|
||||
event, or the margin is only ever added to.
|
||||
|
||||
### 10.4 Rotating with the keyboard up
|
||||
|
||||
Focus a passphrase box, then turn the phone sideways.
|
||||
|
||||
**Pass:** the box is still visible and still focused, and the shell is intact — the activity handles the
|
||||
rotation rather than being recreated, and live shells survive it.
|
||||
|
||||
---
|
||||
|
||||
## Phase 11 — The phone's terminal surface
|
||||
|
||||
Every check here needs a real device for the reason Phase 10's do, plus one of its own: the interesting
|
||||
question on this screen is whether a native `WebView` composites above what Avalonia draws over it, and no
|
||||
headless surface has a native view to answer with. `docs/android-port.md` still records that as unverified;
|
||||
11.2 is the check that settles it.
|
||||
|
||||
### 11.1 A shell gets the screen · **the important one for chrome**
|
||||
|
||||
Open a shell from HOSTS.
|
||||
|
||||
**Pass:** the vault header, the session strip and the four-entry bottom bar are all gone. What is left is
|
||||
one bar — a back arrow, the session pills, a `+` — and then the terminal down to the accessory keys. Press
|
||||
back: all three come back, the tab is still in the strip and its dot is still green.
|
||||
|
||||
**Failure means:** one of the three rows is not bound on `IsShowingPages`, or the terminal is being reached
|
||||
by a route that leaves `Surface` on `Page`.
|
||||
|
||||
### 11.2 The connect menu is not drawn over the renderer · **the important one**
|
||||
|
||||
With a shell showing output, press `+`.
|
||||
|
||||
**Pass:** the terminal's rectangle goes to the canvas colour and the sheet sits over it whole — scrim, three
|
||||
rows and CANCEL, every one of them tappable, none of them sliced down the left edge. Tap the scrim: the
|
||||
terminal comes back with its scrollback intact and the shell still running.
|
||||
|
||||
**Failure means:** `IsTerminalShowing` is not being cleared by `IsConnectSheetOpen` — or, if the sheet is
|
||||
sliced *despite* the rectangle going blank, something else in that Panel is still showing. A sheet that
|
||||
draws over live terminal output is the Android answer to the compositing question, and it means every
|
||||
future sheet on this surface has to collapse the renderer too.
|
||||
|
||||
### 11.3 Back lowers the menu before it leaves the terminal
|
||||
|
||||
With the connect menu open, use the system back gesture.
|
||||
|
||||
**Pass:** the menu closes and the terminal is still showing. A second back leaves the terminal for the
|
||||
screen it was opened over.
|
||||
|
||||
**Failure means:** the guard in `PhoneShell.axaml.cs` is below the surface check rather than above it, and
|
||||
one gesture is spending two levels.
|
||||
|
||||
### 11.4 The two end buttons cannot be pushed off the bar
|
||||
|
||||
Open six or more shells.
|
||||
|
||||
**Pass:** the pills scroll under a fixed back arrow and a fixed `+`; neither ever leaves the screen, and
|
||||
scrolling the pills to either end does not move them.
|
||||
|
||||
**Failure means:** a control was put inside the `ScrollViewer` rather than beside it.
|
||||
|
||||
### 11.5 The text-size buttons are always reachable
|
||||
|
||||
With a shell open, scroll the accessory key row to the far left and the far right.
|
||||
|
||||
**Pass:** `A−` and `A+` stay pinned at the right-hand end throughout, separated from the keys by the
|
||||
hairline, and both are at least 38 tall. At the smallest and largest sizes the one that can do nothing is
|
||||
visibly disabled rather than silently inert.
|
||||
|
||||
**Failure means:** they have been folded into the scrolling row — which is the arrangement the connection
|
||||
line existed to avoid, and the reason it could be removed at all.
|
||||
|
||||
---
|
||||
|
||||
## Phase 12 — Teams: the operations that span two accounts
|
||||
|
||||
The server's own rules are covered by the endpoint suite: teams are renamed, an archive is refused while a
|
||||
vault is in the way, ownership changes hands, and every branch of the invitation claim is driven with
|
||||
@@ -850,7 +970,7 @@ second DodoSSH profile means a second machine, a second OS user, or the same mac
|
||||
Whichever account plays the invitee **must not have signed in to this deployment before** — most of what
|
||||
follows is about what happens the first time it does.
|
||||
|
||||
### 10.1 An invitation becomes a membership at the invitee's first sign-in · **the one worth the most care**
|
||||
### 12.1 An invitation becomes a membership at the invitee's first sign-in · **the one worth the most care**
|
||||
|
||||
1. Sign in as `alice`, make a team, and open its invitations.
|
||||
2. Invite `bob@example.com` as a Member. **Nothing is sent, and nothing should look as though it was** —
|
||||
@@ -866,12 +986,12 @@ follows is about what happens the first time it does.
|
||||
opens.
|
||||
|
||||
**Failure means:** step 5 failing with everything else passing is almost always the `email_verified` claim
|
||||
— go to 10.2 rather than reading the invitation code, because the server is doing exactly what it should.
|
||||
— go to 12.2 rather than reading the invitation code, because the server is doing exactly what it should.
|
||||
Step 6 opening the vault *without* Alice sharing a key would be the far more serious failure: nothing on
|
||||
the server can wrap a vault key, so an item that decrypts after a membership change alone means a key
|
||||
reached that machine by a route this architecture says does not exist.
|
||||
|
||||
### 10.2 An unverified address claims nothing, and the log is the only place that says so
|
||||
### 12.2 An unverified address claims nothing, and the log is the only place that says so
|
||||
|
||||
In Keycloak's admin console, clear **Email verified** on the invitee *before* their first DodoSSH sign-in.
|
||||
Invite that address, then sign in as them.
|
||||
@@ -888,7 +1008,7 @@ can walk into their team. Stop there. If it stays pending after verifying, the c
|
||||
**access** token — check the provider's mappers, and set `Oidc:EmailVerifiedClaim` if it sends the claim
|
||||
under some other name.
|
||||
|
||||
### 10.3 An invitation can be withdrawn until it is taken up
|
||||
### 12.3 An invitation can be withdrawn until it is taken up
|
||||
|
||||
Invite an address, then revoke it before anybody has signed in with it. Then sign in with that address.
|
||||
|
||||
@@ -901,7 +1021,7 @@ accepted one that could be unpicked here would be worse: it is a membership now,
|
||||
revokes their vault key grants and flags every team vault for rekey, which is not what "revoke invitation"
|
||||
should quietly do.
|
||||
|
||||
### 10.4 An address already in the team is refused; an address that merely has an account is not
|
||||
### 12.4 An address already in the team is refused; an address that merely has an account is not
|
||||
|
||||
With Bob in the team, invite `bob@example.com` to it again.
|
||||
|
||||
@@ -915,7 +1035,7 @@ hour on his next request rather than at a sign-in, so give it that long before d
|
||||
endpoint has become a way of asking the server which addresses have accounts on it, answerable by anybody
|
||||
willing to create a team first. See ADR 0009.
|
||||
|
||||
### 10.5 LAST ACTIVE is a real time, and a coarse one · **needs a couple of hours**
|
||||
### 12.5 LAST ACTIVE is a real time, and a coarse one · **needs a couple of hours**
|
||||
|
||||
Use one account and leave the other idle for two or three hours, then read the members table.
|
||||
|
||||
@@ -928,7 +1048,7 @@ user's own overlapping requests failing on a version that moved under them. A va
|
||||
means the refresh is not running on ordinary requests at all, which is the state that made this column
|
||||
impossible to offer honestly before.
|
||||
|
||||
### 10.6 Ownership changes hands in one act
|
||||
### 12.6 Ownership changes hands in one act
|
||||
|
||||
As the owner, transfer ownership to another active member, then read both rows.
|
||||
|
||||
@@ -942,7 +1062,7 @@ member, and to yourself.
|
||||
either one leaves a team that no client can administer back into shape. If your grants were revoked or the
|
||||
vaults are now flagged for rekey, the transfer is removing the outgoing owner rather than demoting them.
|
||||
|
||||
### 10.7 Archiving is refused while the team owns a vault
|
||||
### 12.7 Archiving is refused while the team owns a vault
|
||||
|
||||
With a team that owns at least one vault, try to archive it.
|
||||
|
||||
@@ -953,7 +1073,7 @@ in this product. The team is still in everybody's list afterwards and its vaults
|
||||
everybody holding a key — including the person who pressed it, quietly, and with nothing in the product
|
||||
able to put them back.
|
||||
|
||||
### 10.8 Archiving an empty team takes its memberships and its invitations with it · **needs two accounts**
|
||||
### 12.8 Archiving an empty team takes its memberships and its invitations with it · **needs two accounts**
|
||||
|
||||
Make a team that owns no vaults, add the second account to it, invite a third address, and archive it.
|
||||
|
||||
@@ -965,7 +1085,7 @@ invitations inside the same transaction exists to prevent, and it would happen w
|
||||
nobody is watching. Note that taking the freed slug is correct rather than a defect, and is also the reason
|
||||
an archived team is only restorable by an operator who checks that first.
|
||||
|
||||
### 10.9 Renaming a team, and the slug that does not move
|
||||
### 12.9 Renaming a team, and the slug that does not move
|
||||
|
||||
Rename a team and change its description.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user