git.lucas.co / cce-remote
remote trackpad and keyboard server
git clone https://git.lucas.co/cce-remote.git

commitfadb1d7db8bab28d222639f9c071c36d2fa191a9
parentef8ff67175
authorLucas Galante <[email protected]>
date2026-09-18 23:10
docs: correct five claims CLAUDE.md made about code that changed under it

An audit of CLAUDE.md against the source turned up five statements that no
longer describe this crate. The security core, the delivery design and the
iOS scar tissue all checked out; these did not:

- **Publishing.** "Fetch-only origin; committing locally is publishing" is the
  pre-bare-layer world. `origin` is now `~/git/cce-remote.git`, a real pushable
  bare repo; the static mirror is the `published` remote, and gitsite watches
  the BARE repo's HEAD. Committing without pushing leaves work that looks
  published locally and is not on the site — the state this crate was in when
  the audit ran, along with 20 others.
- **`/shot` is not fetched by the page.** The header-vs-query split is real,
  but it dates from when the page used both endpoints; the live view has ridden
  `/wstream` since the 2026-08-22 rework, and the page references neither.
- **The X-Win rect is not "the numbers the page maps taps through"** — 6e6915c
  removed tap mapping. It is still the right focus/geometry check, for a
  different reason: it is the rect the sources capture and the ring sits in.
- **The ladder does not drop quality before size.** Its first step down is a
  size drop (1400,68 → 1120,68) and quality rises again at 840,58. Documented
  as the alternating ladder it is, and the test whose NAME claims the
  preference is flagged: it only asserts the edge never increases.
- **The tests are not all in `main.rs`** — 19 are, 6 are in `stream.rs` (Slot
  semantics, `adapt()`/`LADDER`), which the sentence listing what is covered
  did not mention.

Three code comments carried the same stale claims and go with them: `/shot`'s
two "the page fetch()es it" notes, `handle_stream`'s doc (still describing the
retired per-frame-grim MJPEG path, ~3 fps, rather than the shared latest-wins
slot at fixed 560/q60), and `LADDER`'s "quality drops before size does".

Comments and prose only; no behavior change, tests unchanged at 25.

Co-Authored-By: Claude Opus 5 <[email protected]>

 CLAUDE.md     | 56 +++++++++++++++++++++++++++++++++++++++++---------------
 src/main.rs   | 22 ++++++++++++----------
 src/stream.rs |  7 ++++---
 3 files changed, 57 insertions(+), 28 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 2811364..ba02837 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -63,9 +63,11 @@ A persistent 6-digit PIN (`~/.config/cce/cce-remote.pin`, 0600, generated from
 `/dev/urandom` on first run, honoring `XDG_CONFIG_HOME`) must arrive as the **first**
 WebSocket frame or the connection closes — with a 10s read timeout so unauthenticated
 peers can't sit on a socket. The HTTP frame endpoints are gated separately, and
-differently, because of a browser constraint: `/shot` takes an `X-Pin` header (the page
-`fetch()`es it), but `/stream` accepts `?pin=` in the query string, because an
-`<img src>` cannot carry headers.
+differently, because of a browser constraint that shaped them when the page still
+used both: `/shot` takes an `X-Pin` header, but `/stream` accepts `?pin=` in the query
+string, because an `<img src>` cannot carry headers. The page uses NEITHER today — the
+live view rides `/wstream` — so both are debug endpoints now, and the split survives
+for the curl recipes below rather than for a browser.
 
 All three gates are pure functions — `auth_frame_ok`, `header_pin_ok`, `query_pin_ok`,
 all over `pin_matches` — so they are unit-tested rather than only reachable through a
@@ -142,9 +144,13 @@ The delivery design (`stream.rs`) makes that failure structurally impossible:
   `drawImage`, not on receipt — so the measured send→ack time covers network + decode +
   paint, which is what the user experiences.
 - That measurement drives an **adaptation ladder** (`LADDER`/`adapt()`): resolution up
-  to 1400px edge when the link is fast, quality degrading before size on the way down,
-  downgrades immediate, upgrades requiring sustained headroom. Encoding happens per
-  *sent* frame at the chosen level.
+  to 1400px edge when the link is fast, downgrades immediate, upgrades requiring
+  sustained headroom. Encoding happens per *sent* frame at the chosen level. The ladder
+  alternates rather than sacrificing one axis first — `(1400,68) → (1120,68) →
+  (1120,55) → (840,58) → (840,46) → (560,48)`, a size drop first, and quality rising
+  again where size falls. `ladder_prefers_resolution_over_quality` does NOT assert the
+  preference its name claims: it only checks that the edge never increases, which a
+  ladder that dropped size at every step would also satisfy.
 - `/wstream` is deliberately a **separate socket from the input WS**: frames are
   30-150KB and input events are bytes; one TCP stream would head-of-line-block pointer
   motion behind every frame.
@@ -240,9 +246,10 @@ The awkward part: **there is no WebSocket client on this machine** (no `websocat
 be driven from a real phone, or by writing a throwaway client.
 
 The pure functions are the exception, and they are where the crate's invariants are
-actually enforced, so they carry all the tests (`cargo test -p cce-remote`, 25 of them,
-in `main.rs`) — `translate()` for what a paired client may say, and the three PIN gates
-for who is paired at all. They cover the accepted shapes and — more to the point —
+actually enforced, so they carry all the tests (`cargo test -p cce-remote`, 25 of them:
+19 in `main.rs`, 6 in `stream.rs`) — `translate()` for what a paired client may say, the
+three PIN gates for who is paired at all, and in `stream.rs` the `Slot`'s latest-wins
+semantics plus `adapt()`/`LADDER`. They cover the accepted shapes and — more to the point —
 everything that
 must be refused: unknown verbs *including the compositor's own command names*,
 malformed and missing arguments, `wf` targets outside `safe_token`, unwhitelisted `cmd`
@@ -265,8 +272,9 @@ curl -D- -o /tmp/shot.png -H "X-Pin: $(cat ~/.config/cce/cce-remote.pin)" \
 ```
 
 `X-Win: <id> <x> <y> <w> <h>` in that reply is the focused window's layout rect — the
-same numbers the page maps taps through, so it is the quickest check that focus
-resolution and geometry agree.
+same rect the frame sources capture and the page places its cursor ring inside, so it
+is the quickest check that focus resolution and geometry agree. (The page does not read
+this header; nothing in the page fetches `/shot` at all.)
 
 **Injected input lands in the live session** — pointer moves steer the user's real
 cursor and keystrokes go into whatever they have focused. To exercise the input path
@@ -280,10 +288,28 @@ startup, so a server is bound to whichever session launched it for its whole lif
 ## Build and lifecycle
 
 `make install` → `ccebuild install --no-build cce-remote`. Never hand-list binaries in
-the Makefile — `cargo metadata` already knows them. This directory is its own git
-repository with a fetch-only origin; committing locally is publishing, via gitsite
-(it is listed in `repos.conf`). No `Cargo.lock` is tracked here, so dependency changes
-need no lockfile refresh.
+the Makefile — `cargo metadata` already knows them. No `Cargo.lock` is tracked here, so
+dependency changes need no lockfile refresh.
+
+**Committing is not publishing — pushing is.** This directory is its own git repository
+whose `origin` is the local *bare* repo `~/git/cce-remote.git` (a real, pushable
+remote). `published` is the old fetch-only static mirror
+`https://git.lucas.co/cce-remote.git`, kept for reference; it never accepted a push
+(dumb HTTP, no receive-pack) and that is exactly why the bare layer exists — see
+`~/.local/bin/git-bare-sync.sh`. `repos.conf` lists the **bare** path, and
+`gitsite.timer` republishes when a listed bare repo's HEAD moves. So the chain is:
+
+```sh
+git commit ...                 # local only
+git push origin master         # this is the publishing step
+                               # gitsite.timer then mirrors it to git.lucas.co
+```
+
+An unpushed commit looks published on this machine and is not on the site. (As of
+2026-09-18 that gap was workspace-wide: 21 crates held unpushed commits, because
+`git-bare-sync.sh` — which does the pushing in bulk — reads `repos.conf` field 2, and
+that field now holds the bare path rather than the work tree, so every listed repo is
+skipped as "not a git work tree".)
 
 **The server does not run in the foreground — it is a user service.** `cce-remote.service`
 ships from this crate root and is installed by `ccebuild` (classified as a user unit by
diff --git a/src/main.rs b/src/main.rs
index db7e508..07d21fe 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -440,12 +440,12 @@ fn handle_ws(stream: TcpStream, pin: &str, ip: std::net::IpAddr, limiter: &RateL
     println!("[cce-remote] client disconnected: {peer}");
 }
 
-/// MJPEG stream of the focused window: multipart/x-mixed-replace with one
-/// JPEG part per grim capture (region = the focused window's layout rect,
-/// re-resolved every few frames so the stream follows focus). ~3 fps for a
-/// full-size window — the screencopy dominates, not the encode. Runs until
-/// the client closes the socket. PIN via X-Pin header or ?pin= query (an
-/// <img src> can't carry headers).
+/// MJPEG stream of the focused window: multipart/x-mixed-replace, one JPEG
+/// part per frame taken from the same latest-wins slot `/wstream` uses (the
+/// producer picks the source and follows focus). Fixed 560/q60, no acks — the
+/// curl-debuggable endpoint, not the page's path. Runs until the client closes
+/// the socket. PIN via X-Pin header or ?pin= query (an <img src> can't carry
+/// headers).
 fn handle_stream(mut stream: TcpStream, request_head: &str, pin: &str, ip: std::net::IpAddr, limiter: &RateLimiter) {
     if !limiter.allow(ip, std::time::Instant::now()) {
         let _ = write!(stream, "HTTP/1.1 429 Too Many Requests\r\nRetry-After: 30\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
@@ -522,15 +522,17 @@ fn handle_http(mut stream: TcpStream, request_head: &str, pin: &str, ip: std::ne
         handle_stream(stream, request_head, pin, ip, limiter);
         return;
     }
-    // /shot: the focused window's screenshot, PIN-gated via the X-Pin header
-    // (the page fetch()es it — an <img src> couldn't carry a header).
+    // /shot: the focused window's screenshot, PIN-gated via the X-Pin header.
+    // A debug endpoint now — the page's live view rides /wstream and nothing
+    // in the page fetches this.
     if request_head.starts_with("GET /shot") {
         if !limiter.allow(ip, std::time::Instant::now()) {
             let _ = write!(stream, "HTTP/1.1 429 Too Many Requests\r\nRetry-After: 30\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
             return;
         }
-        // Header only: the page fetch()es this one, so unlike /stream there is
-        // no reason to let the PIN travel in a URL (where it lands in logs).
+        // Header only: nothing loads this as an <img src>, so unlike /stream
+        // there is no reason to let the PIN travel in a URL (where it lands in
+        // logs).
         if !header_pin_ok(request_head, pin) {
             limiter.record_failure(ip, std::time::Instant::now());
             let _ = write!(stream, "HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
diff --git a/src/stream.rs b/src/stream.rs
index 48984ee..d97b494 100644
--- a/src/stream.rs
+++ b/src/stream.rs
@@ -106,9 +106,10 @@ fn encode(p: &Payload, max_edge: u32, quality: u8) -> Result<Vec<u8>, String> {
 
 // ---- adaptation ------------------------------------------------------------
 
-/// (max_edge px, jpeg quality), best first. Resolution is held as long as
-/// possible — quality drops before size does — because the point of the live
-/// view is READING the window.
+/// (max_edge px, jpeg quality), best first. The point of the live view is
+/// READING the window, so the ladder gives up size grudgingly: it alternates
+/// size and quality steps and lets quality RISE where size falls (840,58 after
+/// 1120,55), rather than exhausting one axis before touching the other.
 pub const LADDER: &[(u32, u8)] = &[
     (1400, 68),
     (1120, 68),