feat: hosts grid + trust-on-first-use UX + settings pane
ci / rust (push) Has been cancelled

The app grows from a dev connect form into a real client shell:

- Home is a grid of saved hosts (UserDefaults-persisted; context menu: Remove / Forget
  Identity), "+" in the toolbar opens the add-host sheet, the stream mode moved into
  Settings (⌘, / gear) — native resolution stays the only mode, no scaling.
- Trust is now explicit: the protocol always supported certificate pinning, but the app
  passed no pin and discarded the observed fingerprint — silently trusting any host.
  First connect now shows the host's SHA-256 fingerprint (compare with the "clients pin
  this fingerprint" line in the host log) over the live-but-blurred stream; the stream
  must pump immediately (the opening IDR is the only guaranteed one), so StreamView gains
  a capturesCursor switch to keep the cursor free while the prompt needs clicking, and
  input capture starts only after confirmation. Trusting pins the fingerprint per host;
  a changed host identity then refuses to connect.
- PUNKTFUNK_AUTOCONNECT keeps working (auto-trusts, doesn't touch the saved hosts).

Host→client authorization (pairing PIN) remains a punktfunk-core roadmap item — the host
still accepts any client that can reach its port.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 16:15:37 +02:00
parent dc42d6a375
commit 5e77731da0
8 changed files with 479 additions and 95 deletions
+11 -5
View File
@@ -39,9 +39,11 @@ What's here, all compiled and tested on macOS (Xcode 26.5 / Swift 6.3):
`vk_to_evdev` consumes Windows VKs), with fractional-delta accumulation so sub-pixel
motion isn't truncated away. Buttons use GameStream ids (1=left … 5=X2); scroll is
WHEEL_DELTA(120)-scaled.
- **`PunktfunkClient`** (development app shell): connect form → stream + input, fps/Mb-s HUD.
(Audio playback and gamepad capture are not wired into the app yet — the connector
surface is there; see notes 56.)
- **`PunktfunkClient`** (the app): hosts grid (saved in UserDefaults), "+" toolbar
sheet to add hosts, stream mode in Settings (⌘,), trust-on-first-use fingerprint prompt
over the live-but-blurred stream → pinned reconnects, fps/Mb-s HUD. (Audio playback and
gamepad capture are not wired into the app yet — the connector surface is there; see
notes 56.)
- **Tests** (`swift test`): byte-level Annex-B units; a real-codec round trip
(VTCompressionSession-encoded HEVC rebuilt as the host's wire shape → `AnnexB`
VTDecompressionSession → pixels); loopback integration against a real local host
@@ -118,8 +120,12 @@ signing, bundle id `io.unom.punktfunk`. Notes:
7. **Trust**: connect once with `pinSHA256: nil` (TOFU), persist `hostFingerprint` keyed
by host, pass it on every later connect — a mismatch throws `.connectFailed`. The host
logs its fingerprint at startup ("clients pin this fingerprint") for out-of-band
verification UX; a PIN-style pairing ceremony is a later punktfunk-core task. `PunktfunkClient`
doesn't persist fingerprints yet — add it alongside the "add host" UX.
verification UX; a PIN-style pairing ceremony is a later punktfunk-core task.
`PunktfunkClient` implements exactly this: explicit fingerprint confirmation on first
connect (input/cursor capture held back until confirmed), pin stored per host
(`HostStore`), "Forget Identity" in the card's context menu for legitimate host
reinstalls. Note the OTHER direction is still open: the host authorizes no one — any
client that reaches the port gets a session (fine on a LAN, not on the internet).
8. **Input capture caveats** (stage 1): GC handlers only fire while the app has focus —
on focus loss `InputCapture` auto-releases everything still held (keys + buttons) so
nothing sticks down host-side. While the stream has focus the LOCAL cursor is hidden