feat(ui): direct-only mode setting (no relay fallback)
New toggle in Settings → "Direct-only mode (no relay fallback)": - Default: OFF (normal behavior, relay fallback on P2P failure) - When ON: connect returns error if P2P fails, with full candidate_diags in the debug log showing why each candidate failed. Call never falls back to relay. Useful for testing NAT traversal — you see the exact failure reason instead of the call silently working through relay. Wired end-to-end: - Settings.directOnly persisted in localStorage - Passed as directOnly param to Rust connect command - connect:path_negotiated shows direct_only flag - connect:direct_only_failed emits on failure with diags Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -187,6 +187,10 @@
|
|||||||
<input id="s-call-debug" type="checkbox" />
|
<input id="s-call-debug" type="checkbox" />
|
||||||
Call flow debug logs (trace every step of a call)
|
Call flow debug logs (trace every step of a call)
|
||||||
</label>
|
</label>
|
||||||
|
<label class="checkbox">
|
||||||
|
<input id="s-direct-only" type="checkbox" />
|
||||||
|
Direct-only mode (no relay fallback — fails if P2P can't connect)
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-section" id="s-call-debug-section" style="display:none">
|
<div class="settings-section" id="s-call-debug-section" style="display:none">
|
||||||
<h3>Call Debug Log</h3>
|
<h3>Call Debug Log</h3>
|
||||||
|
|||||||
@@ -333,13 +333,19 @@ async fn connect(
|
|||||||
// Phase 8 (Tailscale-inspired): peer's port-mapped external
|
// Phase 8 (Tailscale-inspired): peer's port-mapped external
|
||||||
// address from NAT-PMP/PCP/UPnP, carried in CallSetup.
|
// address from NAT-PMP/PCP/UPnP, carried in CallSetup.
|
||||||
peer_mapped_addr: Option<String>,
|
peer_mapped_addr: Option<String>,
|
||||||
|
// Debug: when true, skip relay fallback entirely — the call
|
||||||
|
// fails if direct P2P doesn't connect. Useful for testing NAT
|
||||||
|
// traversal without the relay masking failures.
|
||||||
|
direct_only: Option<bool>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
|
let force_direct = direct_only.unwrap_or(false);
|
||||||
emit_call_debug(&app, "connect:start", serde_json::json!({
|
emit_call_debug(&app, "connect:start", serde_json::json!({
|
||||||
"relay": relay,
|
"relay": relay,
|
||||||
"room": room,
|
"room": room,
|
||||||
"peer_direct_addr": peer_direct_addr,
|
"peer_direct_addr": peer_direct_addr,
|
||||||
"peer_local_addrs": peer_local_addrs,
|
"peer_local_addrs": peer_local_addrs,
|
||||||
"peer_mapped_addr": peer_mapped_addr,
|
"peer_mapped_addr": peer_mapped_addr,
|
||||||
|
"direct_only": force_direct,
|
||||||
}));
|
}));
|
||||||
let mut engine_lock = state.engine.lock().await;
|
let mut engine_lock = state.engine.lock().await;
|
||||||
if engine_lock.is_some() {
|
if engine_lock.is_some() {
|
||||||
@@ -572,7 +578,20 @@ async fn connect(
|
|||||||
"local_direct_ok": local_direct_ok,
|
"local_direct_ok": local_direct_ok,
|
||||||
"peer_direct_ok": peer_direct_ok,
|
"peer_direct_ok": peer_direct_ok,
|
||||||
"chosen_path": format!("{:?}", chosen_path),
|
"chosen_path": format!("{:?}", chosen_path),
|
||||||
|
"direct_only": force_direct,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// direct_only mode: refuse relay fallback
|
||||||
|
if force_direct && !use_direct {
|
||||||
|
let reason = format!(
|
||||||
|
"direct_only: P2P failed (local_ok={local_direct_ok}, peer_ok={peer_direct_ok})"
|
||||||
|
);
|
||||||
|
emit_call_debug(&app, "connect:direct_only_failed", serde_json::json!({
|
||||||
|
"reason": reason,
|
||||||
|
"candidate_diags": race_result.candidate_diags,
|
||||||
|
}));
|
||||||
|
return Err(reason);
|
||||||
|
}
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
?chosen_path,
|
?chosen_path,
|
||||||
use_direct,
|
use_direct,
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ const sAlias = document.getElementById("s-alias") as HTMLInputElement;
|
|||||||
const sOsAec = document.getElementById("s-os-aec") as HTMLInputElement;
|
const sOsAec = document.getElementById("s-os-aec") as HTMLInputElement;
|
||||||
const sDredDebug = document.getElementById("s-dred-debug") as HTMLInputElement;
|
const sDredDebug = document.getElementById("s-dred-debug") as HTMLInputElement;
|
||||||
const sCallDebug = document.getElementById("s-call-debug") as HTMLInputElement;
|
const sCallDebug = document.getElementById("s-call-debug") as HTMLInputElement;
|
||||||
|
const sDirectOnly = document.getElementById("s-direct-only") as HTMLInputElement;
|
||||||
const sCallDebugSection = document.getElementById("s-call-debug-section") as HTMLDivElement;
|
const sCallDebugSection = document.getElementById("s-call-debug-section") as HTMLDivElement;
|
||||||
const sCallDebugLogEl = document.getElementById("s-call-debug-log") as HTMLDivElement;
|
const sCallDebugLogEl = document.getElementById("s-call-debug-log") as HTMLDivElement;
|
||||||
const sCallDebugClearBtn = document.getElementById("s-call-debug-clear") as HTMLButtonElement;
|
const sCallDebugClearBtn = document.getElementById("s-call-debug-clear") as HTMLButtonElement;
|
||||||
@@ -287,6 +288,9 @@ interface Settings {
|
|||||||
/// renders into the rolling Debug Log panel in settings. Off in
|
/// renders into the rolling Debug Log panel in settings. Off in
|
||||||
/// normal mode keeps the GUI quiet but logcat always has a copy.
|
/// normal mode keeps the GUI quiet but logcat always has a copy.
|
||||||
callDebugLogs: boolean;
|
callDebugLogs: boolean;
|
||||||
|
/// Debug: skip relay fallback on direct calls — fail if P2P
|
||||||
|
/// doesn't connect. Useful for testing NAT traversal.
|
||||||
|
directOnly: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadSettings(): Settings {
|
function loadSettings(): Settings {
|
||||||
@@ -301,6 +305,7 @@ function loadSettings(): Settings {
|
|||||||
osAec: true, agc: true, quality: "auto", recentRooms: [],
|
osAec: true, agc: true, quality: "auto", recentRooms: [],
|
||||||
dredDebugLogs: false,
|
dredDebugLogs: false,
|
||||||
callDebugLogs: false,
|
callDebugLogs: false,
|
||||||
|
directOnly: false,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem("wzp-settings");
|
const raw = localStorage.getItem("wzp-settings");
|
||||||
@@ -1175,6 +1180,7 @@ function openSettings() {
|
|||||||
sRoom.value = s.room; sAlias.value = s.alias; sOsAec.checked = s.osAec;
|
sRoom.value = s.room; sAlias.value = s.alias; sOsAec.checked = s.osAec;
|
||||||
sDredDebug.checked = !!s.dredDebugLogs;
|
sDredDebug.checked = !!s.dredDebugLogs;
|
||||||
sCallDebug.checked = !!s.callDebugLogs;
|
sCallDebug.checked = !!s.callDebugLogs;
|
||||||
|
sDirectOnly.checked = !!s.directOnly;
|
||||||
// Show the debug-log panel only when the user has the flag on —
|
// Show the debug-log panel only when the user has the flag on —
|
||||||
// keeps the settings panel short in normal use.
|
// keeps the settings panel short in normal use.
|
||||||
sCallDebugSection.style.display = s.callDebugLogs ? "" : "none";
|
sCallDebugSection.style.display = s.callDebugLogs ? "" : "none";
|
||||||
@@ -1337,6 +1343,7 @@ settingsSave.addEventListener("click", () => {
|
|||||||
s.quality = QUALITY_STEPS[parseInt(sQuality.value)] || "auto";
|
s.quality = QUALITY_STEPS[parseInt(sQuality.value)] || "auto";
|
||||||
s.dredDebugLogs = sDredDebug.checked;
|
s.dredDebugLogs = sDredDebug.checked;
|
||||||
s.callDebugLogs = sCallDebug.checked;
|
s.callDebugLogs = sCallDebug.checked;
|
||||||
|
s.directOnly = sDirectOnly.checked;
|
||||||
saveSettingsObj(s);
|
saveSettingsObj(s);
|
||||||
// Push the new flags to the Rust side immediately so the next
|
// Push the new flags to the Rust side immediately so the next
|
||||||
// frame / call already honors them without waiting for a restart.
|
// frame / call already honors them without waiting for a restart.
|
||||||
@@ -1692,6 +1699,7 @@ listen("signal-event", (event: any) => {
|
|||||||
peerDirectAddr: data.peer_direct_addr ?? null,
|
peerDirectAddr: data.peer_direct_addr ?? null,
|
||||||
peerLocalAddrs: data.peer_local_addrs ?? [],
|
peerLocalAddrs: data.peer_local_addrs ?? [],
|
||||||
peerMappedAddr: data.peer_mapped_addr ?? null,
|
peerMappedAddr: data.peer_mapped_addr ?? null,
|
||||||
|
directOnly: loadSettings().directOnly || false,
|
||||||
});
|
});
|
||||||
showCallScreen();
|
showCallScreen();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
Reference in New Issue
Block a user