diff --git a/.claude/launch.json b/.claude/launch.json
index 456e23a..52ad180 100644
--- a/.claude/launch.json
+++ b/.claude/launch.json
@@ -4,7 +4,7 @@
{
"name": "splatter-tennis",
"runtimeExecutable": "sh",
- "runtimeArgs": ["-c", "PORT=${PORT:-8641} SPLAT_DATA=/tmp/splatter-scores python3 server.py"],
+ "runtimeArgs": ["-c", "PORT=${PORT:-8641} SPLAT_DATA=/tmp/splatter-scores SPLAT_AUTH= python3 server.py"],
"port": 8641,
"autoPort": true
}
diff --git a/PROJECT.md b/PROJECT.md
index d256fe4..5ec4b81 100644
--- a/PROJECT.md
+++ b/PROJECT.md
@@ -5063,6 +5063,42 @@ Release cache version: `main.js?v=353`. Pre-release validation: 142 game tests a
menu browser checks pass. Deploy staging first, verify the served release, then promote the same
commit to production. Staging URL: `https://splatter-tennis-staging.wuwi.one`.
+## Session log: VS A FRIEND — the friend's racket makes a noise, and the duel has a rematch (2026-09-11, v354)
+
+Oliver, two finds off a two-phone session: *"im vs. a friend mode wird der ballsound des gegners
+nicht abgespielt (zumindest deathmatch)"* and *"nach einem vs. a friend duell gibt es kein retry /
+replay button"*. Both are netplay-only by construction, which is why neither had been seen before.
+
+- **THE FRIEND'S STRIKE HAD NOWHERE TO COME FROM.** Every other opponent in the game is simulated
+ locally and cracks through gameplay's own strike line (`sfx('tennis-opponent', …)` in the shared
+ return path, which is why the CPU has always sounded right). The friend's swing runs on nobody's
+ phone but theirs — it arrives as a `shot` message and `netApplyShot` writes the ball straight
+ into the world — so half of every netplay rally was silent, in BOTH netplay modes (the report
+ hedged "at least deathmatch"; it was never mode-specific). One line at the end of `netApplyShot`,
+ after the ball is written so `strikeRate()` pitches off the pace that was actually sent.
+ `gameplay.strikeRate` is exported for it, and `audio.sfxTest` is a new test seam in the shape of
+ `netTest.sent`, so the suite can assert the crack instead of trusting the ear.
+- **THE REMATCH.** The summary's only netplay door was THE CLUBHOUSE: playing the same person again
+ meant both of them walking back through the lobby and reading a fresh code aloud, for the one
+ mode whose whole point is the person opposite. The relay cannot re-pair (its room code is popped
+ on pairing, by design), so the rematch is THE SOCKET THAT IS STILL OPEN — one `again` frame, and
+ both phones reset the run under the decisions the room already carries: same side, same mode,
+ same court, host serves. `net.netAgain()` keeps `G.net` across `restartRun(true)` (the new flag
+ is the whole change to that function) and replays `netFresh()`, which is netStart's own tail
+ split out so the pairing and the rematch cannot drift into two different courts. It is applied,
+ never echoed (the rule every verdict in that file follows), and a 2 s window swallows the second
+ frame when both players tap at once. A dead line refuses, so the button falls through to the
+ clubhouse exactly as before.
+- **The summary grows its second door in netplay** (`online` in render.js): REMATCH filled, THE
+ CLUBHOUSE outlined, NEXT reading ANOTHER DUEL · YOUR FRIEND IS STILL ON THE LINE. It is drawn on
+ the loser's BLED OUT screen too — the phone most likely to want another one.
+- **`.claude/launch.json` now runs the dev server with `SPLAT_AUTH=`**, which server.py's own note
+ always said local dev should do. The shared password (2026-09-08) had been 401-ing the browser
+ pane, and a preview that cannot load is a verification round lost every session.
+- Verified end to end through the real relay in two panes: pair → death match → REMATCH on the host
+ → both courts come up 0-0 with full blood, the friend standing, no gore, "REMATCH — YOUR SERVE"
+ against "REMATCH — THEIR SERVE". 146 tests.
+
# OPEN — leftovers, in rough priority order
Nothing here is broken; these are known gaps and unverified risks.
diff --git a/index.html b/index.html
index 82468bb..f579728 100644
--- a/index.html
+++ b/index.html
@@ -1140,6 +1140,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
REC
-
+