Stop the terminal's WebView painting over the setup screens

The shell layered its setup and unlock screens over the terminal, which does
not work: NativeWebView attaches a real Win32 child HWND through
NativeControlHost, and a child window composites above everything its parent
paints regardless of visual-tree z-order. The cards rendered sliced at the
terminal column's left edge; at the window's default width every one of their
buttons fell inside the WebView's rectangle, so the flow could only be
completed by keyboard, and a click in that region handed Win32 focus to
WebView2 so the text boxes silently stopped accepting keystrokes.

The WebView is now collapsed while the vault is not unlocked. The comment
that previously forbade this — hiding it means never realising it — was
wrong: NativeControlHost creates the native attachment on attach to the
visual tree, never consulting layout or visibility, and NativeWebView replays
a Source assigned before its adapter exists. A collapsed WebView still starts
WebView2, loads the page and lets the renderer attach. Confirmed: 35
msedgewebview2 processes with the control collapsed. What the first
connection after unlocking actually depends on is the existing await on
WaitForRendererAsync, since the data plane drops frames when no renderer is
attached.

Also fixes the second visible defect: the default server URL was
https://localhost:7217, the API's *second* launch profile, while the README,
its appsettings and a plain `dotnet run` all use http://localhost:5233 — so
nothing was listening, and an HTTPS client against a plaintext port reports
"The SSL connection could not be established", which reads as a certificate
problem. The default now matches, a missing scheme is rejected by name
instead of parsing as scheme "localhost", and that specific TLS failure now
suggests http://. Both new tests fail when the fixes are reverted.

Corrections to claims I made earlier and should not have:

- docs/platform-flags.md asserted the opposite of the mechanism above and
  cited an established msedgewebview2 connection as verification. That
  observation was taken while the overlay was showing but, because of this
  very bug, the WebView was uncovered and in plain view — so it confirmed
  only that a visible WebView is realised. A process-level check cannot
  verify a rendering claim. The entry was also filed under "Local cache".
- ITerminalHost was documented as the live seam the app plugs into, with a
  stub standing in for headless tests. It has no implementation anywhere and
  no test uses it; the view navigates the control directly. It also counted
  Avalonia.Controls.WebView and NativeWebView as two interchangeable
  backends when they are one component, with the Linux backend backwards.
- The README claimed the shell's whole path was covered by tests. Its state
  machine is; its layout is covered by nothing, and a headless test could
  not have caught this — headless has no native window, so it would have
  rendered correctly and confirmed the wrong belief.

Verified by screenshotting the running app: the card renders complete and
centred at the default size, with the button clickable.
This commit is contained in:
2026-07-29 13:26:30 +02:00
parent b9e7c258ae
commit 0500e43e02
7 changed files with 206 additions and 35 deletions
@@ -3,7 +3,8 @@
<!--
The terminal data plane. Avalonia-free and WebView-free on purpose: the throughput and
backpressure behaviour is the part most likely to be wrong, and it has to be testable
without a UI toolkit or a browser engine. ITerminalHost is the seam the app plugs into.
without a UI toolkit or a browser engine. What the app plugs into is TerminalWorkspace;
ITerminalHost is a declared shape with no implementation yet, and says so.
-->
<ItemGroup>
+16 -8
View File
@@ -5,16 +5,24 @@ namespace DodoSSH.Client.Terminal;
/// </summary>
/// <remarks>
/// <para>
/// Deliberately tiny. Everything the renderer needs — its files, its connection token, its socket
/// URL — arrives over the loopback HTTP server, so the only thing the host has to do is navigate.
/// That is what keeps three WebView backends interchangeable: the official
/// <c>Avalonia.Controls.WebView</c>, the community <c>NativeWebView</c> whose Linux backend is the
/// more widely installed WebKitGTK, and CEF as the heavyweight escape hatch.
/// <b>Declared, not yet wired.</b> Nothing in the application implements this today: the view assigns
/// <c>NativeWebView.Source</c> directly in <c>MainWindow.axaml.cs</c>, and the headless shell tests
/// substitute <see cref="ITerminalAssetProvider" /> instead — they never need a browser, because the view
/// models do not own one. So swapping WebView backends currently means editing the XAML and its code-behind.
/// This interface records the shape that swap should take; it is not a seam that exists yet, and it should
/// not be cited as one.
/// </para>
/// <para>
/// It is also what makes the terminal testable headlessly. Avalonia's headless platform has no
/// WebView at all, so a stub implementing this interface stands in — and because the interface is one
/// method, the stub cannot drift from the real thing.
/// Deliberately tiny, and that part is worth keeping. Everything the renderer needs — its files, its
/// connection token, its socket URL — arrives over the loopback HTTP server, so the only thing a host has to
/// do is navigate. A backend swap is therefore one method wide however it is eventually wired.
/// </para>
/// <para>
/// The candidates are two, not three: <c>Avalonia.Controls.WebView</c> is the package and
/// <c>NativeWebView</c> is the control it ships, so they are one option — whose Linux backend is WPE WebKit,
/// with WebKitGTK the more widely installed library it is not using — and CEF is the heavyweight escape
/// hatch. An earlier version of this remark counted the package and the control separately and had the
/// Linux backend the wrong way round, which made the interchangeability argument rest on a miscount.
/// </para>
/// </remarks>
public interface ITerminalHost