fix(ui): pre-flight ping + cancel button for register

Two UX issues when the selected relay is unreachable (e.g. user
switched from WiFi to LTE and the LAN relay is gone):

1. Pressing Register blocked the UI for ~30s while the QUIC
   connect timed out against a dead host. No way to abort.
2. No feedback that the relay was unreachable — just a long
   wait followed by a cryptic error.

Fix:

**Pre-flight ping**: before attempting the full register flow,
run `ping_relay` (existing Tauri command, 3s QUIC handshake
timeout). If it fails, immediately show "Server unavailable:
<error>" and re-enable the Register button. No blocking, no
wasted time. If it succeeds, proceed to register_signal.

**Cancel button**: during the register_signal await, the
Register button becomes "Cancel". Tapping it calls `deregister`
which closes the in-flight transport and makes the connect
fail immediately, breaking the await. The button goes back to
"Register on Relay" with a "Registration cancelled" message.

Flow:
  [Register] → "Checking..." (disabled, 3s ping) →
    ping fails → "Server unavailable" (re-enabled)
    ping ok → "Cancel" (enabled, register in flight) →
      user taps Cancel → "Registration cancelled" (re-enabled)
      register succeeds → registered panel shown
      register fails → error shown (re-enabled)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-04-12 09:13:35 +04:00
parent 6cd61fc63b
commit 847699bf66

View File

@@ -1450,11 +1450,51 @@ clearHistoryBtn.addEventListener("click", async () => {
} catch (e) { console.error(e); }
});
// Track whether a registration is in flight so the same button
// can toggle between "Register" and "Cancel". The cancel path
// calls deregister which closes the transport and makes the
// in-flight connect fail, breaking the await cleanly.
let registerInFlight = false;
registerBtn.addEventListener("click", async () => {
// ── Cancel path: user tapped the button while registration
// is in flight (it says "Cancel") → tear down the attempt
// so we don't block for 30s on an unreachable relay.
if (registerInFlight) {
registerInFlight = false;
try { await invoke("deregister"); } catch {}
registerBtn.textContent = "Register on Relay";
registerBtn.disabled = false;
connectError.textContent = "Registration cancelled";
return;
}
const relay = getSelectedRelay();
if (!relay) { connectError.textContent = "No relay selected"; return; }
connectError.textContent = "";
// ── Pre-flight ping: quick 3s QUIC handshake to check if
// the relay is reachable BEFORE committing to the full
// register flow (which takes ~10s to time out against a dead
// host). If the ping fails, show "server unavailable"
// immediately without blocking.
registerBtn.textContent = "Checking...";
registerBtn.disabled = true;
registerBtn.textContent = "Registering...";
try {
await invoke("ping_relay", { relay: relay.address });
} catch (e: any) {
connectError.textContent = `Server unavailable: ${String(e)}`;
registerBtn.disabled = false;
registerBtn.textContent = "Register on Relay";
return;
}
// ── Register path: ping succeeded, proceed with the full
// registration. Show "Cancel" on the button so the user
// can bail if the relay goes unreachable mid-handshake.
registerInFlight = true;
registerBtn.disabled = false;
registerBtn.textContent = "Cancel";
try {
const fp = await invoke<string>("register_signal", { relay: relay.address });
registerBtn.classList.add("hidden");
@@ -1462,9 +1502,14 @@ registerBtn.addEventListener("click", async () => {
callStatusText.textContent = `Your fingerprint: ${fp}`;
refreshHistory();
} catch (e: any) {
connectError.textContent = String(e);
if (registerInFlight) {
// Real failure, not a user cancel
connectError.textContent = String(e);
}
registerBtn.disabled = false;
registerBtn.textContent = "Register on Relay";
} finally {
registerInFlight = false;
}
});