fix(call): default Accept to AcceptTrusted + add log Copy/Share buttons
## Accept button regression — diagnosed from a user log
Field report: incoming call → callee taps Accept → debug log
shows the dual-path race being skipped with
`connect:dual_path_skipped {"has_own":false,"has_peer":true,
"role":"None"}` and the call falling to relay-only on the
callee side.
Root cause: the Accept button was calling `answer_call` with
`mode: 2` which falls through to `AcceptGeneric` (privacy
mode). By design, privacy mode SKIPS the reflex query on the
callee so the callee's IP stays hidden from the caller — but
the side effect is that `own_reflex_addr` never gets cached in
`SignalState`. When `connect` runs a moment later, it sees
`own_reflex_addr = None`, can't compute the deterministic role
for the dual-path race, and falls back to relay.
For a normal VoIP app where P2P is the desired default, the
right behavior is `AcceptTrusted` — which queries reflect,
advertises the callee's addr in the answer, and enables direct
P2P. Privacy mode can come back as a dedicated second button
if anyone actually needs it.
Changed `acceptCallBtn` click handler from `mode: 2` to
`mode: 1`. The next call from a Phase-5 APK should show
`connect:dual_path_race_start` + `connect:dual_path_race_won
{"path":"Direct"}` on a cone-NAT-to-cone-NAT pair.
## Debug log export — new Copy / Share buttons
Field-testing the GUI debug log required me to keep asking the
user to type out what they saw. Added two new buttons next to
Clear:
- **Copy log** — serialises the rolling buffer as plain text
(same HH:MM:SS.mmm format the on-screen panel uses) and
writes to `navigator.clipboard`. Falls back to the old
selection-based `execCommand("copy")` for WebViews that
refuse the new API without a permission prompt.
- **Share** — tries the Web Share API (`navigator.share(...)`)
first. On Android WebView this opens the system share sheet
so the user can send the text straight to a messaging app.
Falls back to clipboard copy on WebViews that don't expose
navigator.share (most desktop ones). Also falls back if the
user cancels the share sheet.
Flash status line below the buttons shows a 2.5s confirmation
("✓ Copied 47 entries") or an error hint. The log is plain
text so anyone can paste a log fragment into a message and
send it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -181,7 +181,12 @@
|
||||
<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>
|
||||
<div style="display:flex;gap:6px;margin-top:6px">
|
||||
<button id="s-call-debug-copy" class="secondary-btn" style="flex:1">Copy log</button>
|
||||
<button id="s-call-debug-share" class="secondary-btn" style="flex:1">Share</button>
|
||||
<button id="s-call-debug-clear" class="secondary-btn" style="flex:1">Clear log</button>
|
||||
</div>
|
||||
<small id="s-call-debug-copy-status" style="display:block;margin-top:4px;color:var(--text-dim);font-size:10px"></small>
|
||||
<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
|
||||
|
||||
@@ -206,6 +206,9 @@ const sCallDebug = document.getElementById("s-call-debug") as HTMLInputElement;
|
||||
const sCallDebugSection = document.getElementById("s-call-debug-section") as HTMLDivElement;
|
||||
const sCallDebugLogEl = document.getElementById("s-call-debug-log") as HTMLDivElement;
|
||||
const sCallDebugClearBtn = document.getElementById("s-call-debug-clear") as HTMLButtonElement;
|
||||
const sCallDebugCopyBtn = document.getElementById("s-call-debug-copy") as HTMLButtonElement;
|
||||
const sCallDebugShareBtn = document.getElementById("s-call-debug-share") as HTMLButtonElement;
|
||||
const sCallDebugCopyStatus = document.getElementById("s-call-debug-copy-status") as HTMLElement;
|
||||
const sReflectedAddr = document.getElementById("s-reflected-addr") as HTMLSpanElement;
|
||||
const sReflectBtn = document.getElementById("s-reflect-btn") as HTMLButtonElement;
|
||||
const sNatType = document.getElementById("s-nat-type") as HTMLSpanElement;
|
||||
@@ -617,6 +620,100 @@ sCallDebugClearBtn.addEventListener("click", () => {
|
||||
sCallDebugLogEl.textContent = "";
|
||||
});
|
||||
|
||||
/// Serialise the rolling call-debug buffer as plain text for
|
||||
/// copy/share. One entry per line, HH:MM:SS.mmm + step +
|
||||
/// compact JSON details. Same format the on-screen panel uses.
|
||||
function formatCallDebugLog(): string {
|
||||
return callDebugBuffer
|
||||
.map((e) => {
|
||||
const iso = new Date(e.ts_ms).toISOString().slice(11, 23);
|
||||
const details =
|
||||
e.details && Object.keys(e.details).length > 0
|
||||
? " " + JSON.stringify(e.details)
|
||||
: "";
|
||||
return `${iso} ${e.step}${details}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/// One-shot status helper for the copy/share buttons.
|
||||
function flashCallDebugStatus(msg: string, isError: boolean = false) {
|
||||
sCallDebugCopyStatus.textContent = msg;
|
||||
sCallDebugCopyStatus.style.color = isError ? "var(--yellow)" : "var(--green)";
|
||||
setTimeout(() => {
|
||||
sCallDebugCopyStatus.textContent = "";
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
sCallDebugCopyBtn.addEventListener("click", async () => {
|
||||
const text = formatCallDebugLog();
|
||||
if (!text) {
|
||||
flashCallDebugStatus("Log is empty", true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
flashCallDebugStatus(`✓ Copied ${callDebugBuffer.length} entries`);
|
||||
} catch (e) {
|
||||
// Some WebViews refuse clipboard access without a user
|
||||
// permission prompt; fall back to a selection-based copy.
|
||||
try {
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = text;
|
||||
ta.style.position = "fixed";
|
||||
ta.style.top = "0";
|
||||
ta.style.left = "0";
|
||||
ta.style.opacity = "0";
|
||||
document.body.appendChild(ta);
|
||||
ta.focus();
|
||||
ta.select();
|
||||
const ok = document.execCommand("copy");
|
||||
document.body.removeChild(ta);
|
||||
if (ok) {
|
||||
flashCallDebugStatus(`✓ Copied ${callDebugBuffer.length} entries`);
|
||||
} else {
|
||||
throw new Error("execCommand returned false");
|
||||
}
|
||||
} catch (e2) {
|
||||
flashCallDebugStatus(`⚠ Copy failed: ${String(e2)}`, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sCallDebugShareBtn.addEventListener("click", async () => {
|
||||
const text = formatCallDebugLog();
|
||||
if (!text) {
|
||||
flashCallDebugStatus("Log is empty", true);
|
||||
return;
|
||||
}
|
||||
// Try the Web Share API first — on Android WebView, this opens
|
||||
// the standard Share sheet and the user can send the text to
|
||||
// any messaging app. Falls back to clipboard copy if the
|
||||
// WebView doesn't expose navigator.share (most desktop
|
||||
// WebViews don't).
|
||||
const nav: any = navigator;
|
||||
if (nav.share) {
|
||||
try {
|
||||
await nav.share({
|
||||
title: "WarzonePhone debug log",
|
||||
text,
|
||||
});
|
||||
flashCallDebugStatus(`✓ Shared ${callDebugBuffer.length} entries`);
|
||||
return;
|
||||
} catch (e) {
|
||||
// User cancelled or WebView rejected — fall through to
|
||||
// clipboard copy as a best-effort.
|
||||
console.debug("share failed, falling back to clipboard", e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
flashCallDebugStatus(`✓ Copied (no share API)`);
|
||||
} catch (e) {
|
||||
flashCallDebugStatus(`⚠ Share + copy both failed`, true);
|
||||
}
|
||||
});
|
||||
|
||||
// Load fingerprint + alias + git hash + render identicon
|
||||
interface AppInfo { git_hash: string; alias: string; fingerprint: string; data_dir: string }
|
||||
|
||||
@@ -1331,7 +1428,15 @@ acceptCallBtn.addEventListener("click", async () => {
|
||||
ringer.stop();
|
||||
const status = await invoke<any>("get_signal_status");
|
||||
if (status.incoming_call_id) {
|
||||
await invoke("answer_call", { callId: status.incoming_call_id, mode: 2 });
|
||||
// mode=1 → AcceptTrusted — enables P2P direct path by
|
||||
// querying + advertising the callee's reflex addr in the
|
||||
// answer. The alternative is mode=2 → AcceptGeneric
|
||||
// (privacy mode) which intentionally skips the reflex query
|
||||
// to keep the callee's IP hidden from the caller but forces
|
||||
// the call onto the relay path. Default to trusted so the
|
||||
// Accept button gets real P2P; privacy can be a future
|
||||
// dedicated button if anyone needs it.
|
||||
await invoke("answer_call", { callId: status.incoming_call_id, mode: 1 });
|
||||
incomingCallPanel.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user