Two features in one commit because they ship and test together:
Phase 3.5 closes the hole-punching loop and the call-flow debug
logs give the user live visibility into every step of a call so
real-hardware testing of the new P2P path is debuggable.
## Phase 3.5 — dual-path QUIC connect race
Completes the hole-punching work Phase 3 scaffolded. On receiving
a CallSetup with peer_direct_addr, the client now actually races a
direct QUIC handshake against the relay dial and uses whichever
completes first. Symmetric role assignment avoids the two-conns-
per-call problem:
- Both peers compare `own_reflex_addr` vs `peer_reflex_addr`
lexicographically.
- Smaller addr → **Acceptor** (A-role): builds a server-capable
dual endpoint, awaits an incoming QUIC session. Does NOT dial.
- Larger addr → **Dialer** (D-role): builds a client-only
endpoint, dials the peer's addr with `call-<id>` SNI. Does NOT
listen.
- Both sides always dial the relay in parallel as fallback.
- `tokio::select!` with `biased` preference for direct, `tokio::pin!`
so each branch can await the losing opposite as fallback.
- Direct timeout 2s, relay fallback timeout 5s (so 7s worst case
from CallSetup to "no media path" error).
New crate module `wzp_client::dual_path::{race, WinningPath}`
(moved here from desktop/src-tauri so it's testable from a
workspace test). `determine_role` in `wzp_client::reflect` is
pure-function and unit-tested.
### CallEngine integration
- New `pre_connected_transport: Option<Arc<QuinnTransport>>` arg
on both android + desktop `CallEngine::start` branches. Skips
the internal wzp_transport::connect step when Some. Backward-
compat: None keeps Phase 0 relay-only behavior.
- `connect` Tauri command reads own_reflex_addr from SignalState,
computes role, runs the race, passes the winning transport
into CallEngine. If ANY input is missing (no peer addr, no own
addr, equal addrs), falls back to classic relay path —
identical to pre-Phase-3.5 behavior.
### Tests (9 new, all passing)
- 6 unit tests for `determine_role` truth table in
`wzp-client/src/reflect.rs` (smaller=Acceptor, larger=Dialer,
port-only diff, equal, missing-side, symmetry)
- 3 integration tests in `crates/wzp-client/tests/dual_path.rs`:
* `dual_path_direct_wins_on_loopback` — two-endpoint test
rig, Dialer wins direct path vs loopback mock relay
* `dual_path_relay_wins_when_direct_is_dead` — dead peer
port, 2s direct timeout, relay fallback wins
* `dual_path_errors_cleanly_when_both_paths_dead` — <10s
error, no hang
## GUI call-flow debug logs
Runtime-toggled structured events at every step of a call so the
user can see where a call progressed or stalled on real hardware.
Modeled on the existing DRED_VERBOSE_LOGS pattern.
### Rust side
- `static CALL_DEBUG_LOGS: AtomicBool` + `emit_call_debug(&app,
step, details)` helper. Always logs via `tracing::info!`
(logcat always has a copy); GUI Tauri `call-debug-log` event
only fires when the flag is on.
- Tauri commands `set_call_debug_logs` / `get_call_debug_logs`.
### Instrumented steps (24 emit_call_debug sites)
- `register_signal`: start, identity loaded, endpoint created,
connect failed/ok, RegisterPresence sent, ack received/failed,
recv loop spawning
- Recv loop: CallRinging, DirectCallOffer (w/ caller_reflexive_addr),
DirectCallAnswer (w/ callee_reflexive_addr), CallSetup (w/
peer_direct_addr), Hangup
- `place_call`: start, reflect query start/ok/none, offer sent,
send failed
- `answer_call`: start, reflect query start/ok/none or privacy
skip, answer sent, send failed
- `connect`: start, dual_path_race_start (w/ role), won (w/
path), failed, skipped (w/ reasons), call_engine_starting/
started/failed
### JS side
- New `callDebugLogs: boolean` field on Settings type.
- Boot-time hydrate of the Rust flag from localStorage so the
choice survives restarts (like `dredDebugLogs`).
- Settings panel: new "Call flow debug logs" checkbox alongside
the DRED toggle.
- New "Call Debug Log" section that ONLY shows when the flag is
on. Rolling in-memory buffer of the last 200 events, rendered
as monospace `HH:MM:SS.mmm step {details}` lines with auto-
scroll and a Clear button.
- `listen("call-debug-log", ...)` subscribed at app startup,
appends to the buffer, re-renders on every event.
Full workspace test goes from 404 → 413 passing. Clippy clean
on touched crates.
PRD: .taskmaster/docs/prd_phase35_dual_path_race.txt
Tasks: 61-69 all completed
Next: APK + desktop build carrying everything — Phase 2 NAT
detect, Phase 3 advertising, Phase 3.5 dual-path + call debug
logs, plus the earlier Android first-join diagnostics — so the
user can validate the P2P path on real hardware with live
per-step visibility into where any failures happen.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
277 lines
12 KiB
HTML
277 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta
|
|
name="viewport"
|
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
|
/>
|
|
<title>WarzonePhone</title>
|
|
<link rel="stylesheet" href="/src/style.css" />
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<!-- Connect screen -->
|
|
<div id="connect-screen">
|
|
<h1>WarzonePhone</h1>
|
|
<p class="subtitle">Encrypted Voice</p>
|
|
<div class="form">
|
|
<label>Relay
|
|
<button id="relay-selected" class="relay-selected" type="button">
|
|
<span id="relay-dot" class="dot"></span>
|
|
<span id="relay-label">Select relay...</span>
|
|
<span class="arrow">⚙</span>
|
|
</button>
|
|
</label>
|
|
<label>Room
|
|
<input id="room" type="text" value="general" />
|
|
</label>
|
|
<label>Alias
|
|
<input id="alias" type="text" placeholder="your name" />
|
|
</label>
|
|
<div class="form-row">
|
|
<label class="checkbox">
|
|
<input id="os-aec" type="checkbox" checked />
|
|
OS Echo Cancel
|
|
</label>
|
|
<button id="settings-btn-home" class="icon-btn" title="Settings (Cmd+,)">⚙</button>
|
|
</div>
|
|
<!-- Mode toggle -->
|
|
<div class="mode-toggle" style="display:flex;gap:8px;margin-bottom:8px;">
|
|
<button id="mode-room" class="mode-btn active" style="flex:1">Room</button>
|
|
<button id="mode-direct" class="mode-btn" style="flex:1">Direct Call</button>
|
|
</div>
|
|
|
|
<!-- Room mode (default) -->
|
|
<div id="room-mode">
|
|
<button id="connect-btn" class="primary">Connect</button>
|
|
</div>
|
|
|
|
<!-- Direct call mode -->
|
|
<div id="direct-mode" class="hidden">
|
|
<button id="register-btn" class="primary" style="background:#2196F3">Register on Relay</button>
|
|
<div id="direct-registered" class="hidden" style="margin-top:12px">
|
|
<div class="direct-registered-header">
|
|
<p style="color:var(--green);font-size:13px;margin:0">✅ Registered — waiting for calls</p>
|
|
<button id="deregister-btn" class="secondary-btn small">Deregister</button>
|
|
</div>
|
|
<div id="incoming-call-panel" class="hidden" style="background:#1B5E20;padding:12px;border-radius:8px;margin:8px 0">
|
|
<p style="font-weight:bold;margin:0 0 4px 0">Incoming Call</p>
|
|
<p id="incoming-caller" style="font-size:12px;opacity:0.8;margin:0 0 8px 0">From: unknown</p>
|
|
<div style="display:flex;gap:8px">
|
|
<button id="accept-call-btn" style="flex:1;background:var(--green);color:white;border:none;padding:8px;border-radius:6px;cursor:pointer">Accept</button>
|
|
<button id="reject-call-btn" style="flex:1;background:var(--red);color:white;border:none;padding:8px;border-radius:6px;cursor:pointer">Reject</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent contacts -->
|
|
<div id="recent-contacts-section" class="hidden">
|
|
<div class="history-header">Recent contacts</div>
|
|
<div id="recent-contacts-list" class="history-list"></div>
|
|
</div>
|
|
|
|
<!-- Call history -->
|
|
<div id="call-history-section" class="hidden">
|
|
<div class="history-header">
|
|
History
|
|
<button id="clear-history-btn" class="link-btn">clear</button>
|
|
</div>
|
|
<div id="call-history-list" class="history-list"></div>
|
|
</div>
|
|
|
|
<label style="margin-top:8px">Call by fingerprint
|
|
<input id="target-fp" type="text" placeholder="xxxx:xxxx:xxxx:..." />
|
|
</label>
|
|
<button id="call-btn" class="primary" style="margin-top:8px">Call</button>
|
|
<p id="call-status-text" style="color:var(--yellow);font-size:13px;margin-top:4px"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<p id="connect-error" class="error"></p>
|
|
</div>
|
|
<div class="identity-info">
|
|
<span id="my-identicon"></span>
|
|
<span id="my-fingerprint" class="fp-display"></span>
|
|
</div>
|
|
<div class="recent-rooms" id="recent-rooms"></div>
|
|
</div>
|
|
|
|
<!-- In-call screen -->
|
|
<div id="call-screen" class="hidden">
|
|
<div class="call-header">
|
|
<div class="call-header-row">
|
|
<div id="room-name" class="room-name"></div>
|
|
<button id="settings-btn-call" class="icon-btn small" title="Settings (Cmd+,)">⚙</button>
|
|
</div>
|
|
<div class="call-meta">
|
|
<span id="call-status" class="status-dot"></span>
|
|
<span id="call-timer" class="call-timer">0:00</span>
|
|
</div>
|
|
</div>
|
|
<div class="level-meter">
|
|
<div id="level-bar" class="level-bar-fill"></div>
|
|
</div>
|
|
<div id="participants" class="participants"></div>
|
|
<div class="controls">
|
|
<button id="mic-btn" class="control-btn" title="Toggle Mic (m)">
|
|
<span class="icon" id="mic-icon">Mic</span>
|
|
</button>
|
|
<button id="hangup-btn" class="control-btn hangup" title="Hang Up (q)">
|
|
<span class="icon">End</span>
|
|
</button>
|
|
<button id="spk-btn" class="control-btn" title="Toggle Speaker (s)">
|
|
<span class="icon" id="spk-icon">Spk</span>
|
|
</button>
|
|
</div>
|
|
<div id="stats" class="stats"></div>
|
|
</div>
|
|
|
|
<!-- Settings panel -->
|
|
<div id="settings-panel" class="hidden">
|
|
<div class="settings-card">
|
|
<div class="settings-header">
|
|
<h2>Settings</h2>
|
|
<button id="settings-close" class="icon-btn">×</button>
|
|
</div>
|
|
<div class="settings-section">
|
|
<h3>Connection</h3>
|
|
<label>Default Room
|
|
<input id="s-room" type="text" />
|
|
</label>
|
|
<label>Alias
|
|
<input id="s-alias" type="text" />
|
|
</label>
|
|
</div>
|
|
<div class="settings-section">
|
|
<h3>Audio</h3>
|
|
<div class="quality-control">
|
|
<div class="quality-header">
|
|
<span class="setting-label">QUALITY</span>
|
|
<span id="s-quality-label" class="quality-label">Auto</span>
|
|
</div>
|
|
<input id="s-quality" type="range" min="0" max="7" step="1" value="3" class="quality-slider" />
|
|
<div class="quality-ticks">
|
|
<span>64k</span>
|
|
<span>48k</span>
|
|
<span>32k</span>
|
|
<span>Auto</span>
|
|
<span>24k</span>
|
|
<span>6k</span>
|
|
<span>C2</span>
|
|
<span>1.2k</span>
|
|
</div>
|
|
</div>
|
|
<label class="checkbox">
|
|
<input id="s-os-aec" type="checkbox" />
|
|
OS Echo Cancellation (macOS VoiceProcessingIO)
|
|
</label>
|
|
<label class="checkbox">
|
|
<input id="s-agc" type="checkbox" checked />
|
|
Automatic Gain Control
|
|
</label>
|
|
<label class="checkbox">
|
|
<input id="s-dred-debug" type="checkbox" />
|
|
DRED debug logs (verbose, dev only)
|
|
</label>
|
|
<label class="checkbox">
|
|
<input id="s-call-debug" type="checkbox" />
|
|
Call flow debug logs (trace every step of a call)
|
|
</label>
|
|
</div>
|
|
<div class="settings-section" id="s-call-debug-section" style="display:none">
|
|
<h3>Call Debug Log</h3>
|
|
<div id="s-call-debug-log" style="max-height:220px;overflow-y:auto;background:#0a0a0a;color:#e0e0e0;font-family:ui-monospace,Menlo,Monaco,'Courier New',monospace;font-size:10px;padding:6px;border-radius:4px;line-height:1.4;white-space:pre-wrap"></div>
|
|
<button id="s-call-debug-clear" class="secondary-btn" style="margin-top:6px">Clear log</button>
|
|
<small style="color:var(--text-dim);display:block;margin-top:4px">
|
|
Rolling buffer of the last 200 call-flow events. Turned off by
|
|
default — the GUI overlay only populates when the checkbox above
|
|
is on, but logcat (adb) always keeps a copy regardless.
|
|
</small>
|
|
</div>
|
|
<div class="settings-section">
|
|
<h3>Identity</h3>
|
|
<div class="setting-row">
|
|
<span class="setting-label">Fingerprint</span>
|
|
<span id="s-fingerprint" class="fp-display-large"></span>
|
|
</div>
|
|
<div class="setting-row">
|
|
<span class="setting-label">Identity file</span>
|
|
<span class="fp-display">~/.wzp/identity</span>
|
|
</div>
|
|
</div>
|
|
<div class="settings-section">
|
|
<h3>Network</h3>
|
|
<div class="setting-row">
|
|
<span class="setting-label">Public address</span>
|
|
<span id="s-reflected-addr" class="fp-display">(not queried)</span>
|
|
<button id="s-reflect-btn" class="secondary-btn">Detect</button>
|
|
</div>
|
|
<small style="color:var(--text-dim);display:block;margin-top:4px">
|
|
Asks the registered relay to echo back the IP:port it sees for this
|
|
connection (QUIC-native NAT reflection, replaces STUN).
|
|
</small>
|
|
<div class="setting-row" style="margin-top:10px">
|
|
<span class="setting-label">NAT type</span>
|
|
<span id="s-nat-type" class="fp-display">(not detected)</span>
|
|
<button id="s-nat-detect-btn" class="secondary-btn">Detect NAT</button>
|
|
</div>
|
|
<div id="s-nat-probes" style="margin-top:6px;font-size:11px;color:var(--text-dim)"></div>
|
|
<small style="color:var(--text-dim);display:block;margin-top:4px">
|
|
Probes every configured relay in parallel and compares the results
|
|
to classify the NAT: cone (P2P viable), symmetric (must relay),
|
|
multiple, or unknown.
|
|
</small>
|
|
</div>
|
|
<div class="settings-section">
|
|
<h3>Recent Rooms</h3>
|
|
<div id="s-recent-rooms" class="recent-rooms-list"></div>
|
|
<button id="s-clear-recent" class="secondary-btn">Clear History</button>
|
|
</div>
|
|
<button id="settings-save" class="primary">Save</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Manage Relays dialog -->
|
|
<div id="relay-dialog" class="hidden">
|
|
<div class="settings-card relay-dialog-card">
|
|
<div class="settings-header">
|
|
<h2>Manage Relays</h2>
|
|
<button id="relay-dialog-close" class="icon-btn">×</button>
|
|
</div>
|
|
<div id="relay-dialog-list" class="relay-dialog-list"></div>
|
|
<div class="relay-add-row">
|
|
<div class="relay-add-inputs">
|
|
<input id="relay-add-name" type="text" placeholder="Name" />
|
|
<input id="relay-add-addr" type="text" placeholder="host:port" />
|
|
</div>
|
|
<button id="relay-add-btn" class="primary">Add Relay</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Key changed warning dialog -->
|
|
<div id="key-warning" class="hidden">
|
|
<div class="settings-card key-warning-card">
|
|
<div class="key-warning-icon">⚠</div>
|
|
<h2>Server Key Changed</h2>
|
|
<p class="key-warning-text">The relay's identity has changed since you last connected. This usually happens when the server was restarted, but could also indicate a security issue.</p>
|
|
<div class="key-warning-fps">
|
|
<div class="key-fp-row">
|
|
<span class="key-fp-label">Previously known</span>
|
|
<code id="kw-old-fp" class="key-fp"></code>
|
|
</div>
|
|
<div class="key-fp-row">
|
|
<span class="key-fp-label">New key</span>
|
|
<code id="kw-new-fp" class="key-fp"></code>
|
|
</div>
|
|
</div>
|
|
<div class="key-warning-actions">
|
|
<button id="kw-accept" class="primary">Accept New Key</button>
|
|
<button id="kw-cancel" class="secondary-btn">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script type="module" src="/src/main.ts"></script>
|
|
</body>
|
|
</html>
|