Theseus: fix collision blank-page + broken-BCDN-open bugs

Two related bugs both caused by the tab's will-navigate handler racing with
programmatic loads:

Bug A (chip switch BCDN -> ICANN shows blank page, BCDN-priority mode):
  fallbackToWeb() calls webContents.loadURL('https://<host>/') to serve the
  ICANN version. That fired will-navigate, which saw a dotted host, ran
  isBnsHost() -> true, and RE-INVOKED navigateTab() recursively — but the
  transient='icann' override had already been consumed, so the recursive call
  fell back to bcnr-first, canceling the fallback mid-flight. Tab showed blank
  because both loads collided.
  Fix: mark programmatic loads with t.internalNav so will-navigate skips them.

Bug B ('Ask each time' -> Open BCDN doesn't load):
  Was going through a meta-refresh from bns://collision-choose/ to
  bns://<host>/?_collision=bcnr. The meta-refresh bypassed navigateTab, so the
  chrome/prov state was never updated (address bar, badges stayed stale).
  Fix: will-navigate now catches bns://collision-choose/ FIRST, applies the
  remember flag, sets t.collisionOverride, and routes via navigateTab so
  chrome + prov update correctly.
  Removed the serveBns collision-choose handler + the ?_collision URL-param
  path in loadBns (both dead now that will-navigate handles it).

Result:
- Soft-mode 'Open with...' -> Open BCDN loads the BCDN site cleanly.
- Chip switcher flips BCDN <-> ICANN with no blank flash, correct chrome.
This commit is contained in:
Local Dev 2026-08-02 15:21:21 +02:00
parent bf54c245b7
commit c3f771ac1c

58
main.js
View file

@ -532,34 +532,8 @@ async function serveBns(request) {
const host = url.hostname.toLowerCase();
const reqPath = decodeURIComponent(url.pathname) || "/";
// Special host: apply a soft-mode "Open with…" choice submitted by
// collision.html, then redirect the tab to the actual target. Persistent
// memory (byName/byTld) is written HERE; the one-shot BCDN choice rides
// along via a ?_collision=bcnr param that loadBns consumes.
if (host === "collision-choose") {
const p = url.searchParams;
const target = String(p.get("host") || "").toLowerCase();
const cTld = String(p.get("tld") || "").toLowerCase();
const cChoice = p.get("choice");
const cRem = p.get("remember") || "no";
const rest = p.get("resturl") || "/";
if (cChoice === "bcnr" || cChoice === "icann") {
rememberCollision(target, cTld, cChoice, cRem);
}
let dest;
if (cChoice === "icann") {
dest = "https://" + target + rest;
} else {
// BCNR/BCDN: bounce back through the bns:// resolver with a one-shot
// marker so loadBns won't re-prompt for this specific load.
const sep = rest.includes("?") ? "&" : "?";
dest = "bns://" + target + rest + sep + "_collision=bcnr";
}
const html = `<!doctype html><meta charset="utf-8"><title>Opening…</title>` +
`<meta http-equiv="refresh" content="0;url=${dest.replace(/"/g, "&quot;")}">` +
`<body style="background:#0f1116;color:#9ca3af;font:14px system-ui;padding:24px">Opening ${target}…</body>`;
return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } });
}
// (bns://collision-choose/ is handled by the will-navigate listener attached
// to each tab — it fires BEFORE the request reaches this protocol handler.)
let rec = entries.get(host);
if (!rec) { try { await resolveHost(host); } catch {} rec = entries.get(host); }
@ -778,7 +752,30 @@ function createTab(initial, opts = {}) {
wc.on("did-stop-loading", () => setLoading(tab, false));
wc.on("will-navigate", (e, u) => {
try {
// Skip our own programmatic loads. fallbackToWeb calls loadURL("https://<host>/")
// and that host is often a BCNR-registered dotted name — without this guard,
// isBnsHost() would send us right back into navigateTab, canceling the
// fallback (blank-page bug 2026-08-02).
if (tab.internalNav) return;
const parsed = new URL(u);
// Intercept the collision-choose posted by the in-tab "Open with…" page,
// apply the remember flag, set a one-shot transient override so loadBns
// doesn't re-prompt, and route via navigateTab so chrome/prov stay in sync.
if (parsed.protocol === "bns:" && parsed.hostname === "collision-choose") {
e.preventDefault();
const p = parsed.searchParams;
const target = String(p.get("host") || "").toLowerCase();
const cTld = String(p.get("tld") || "").toLowerCase();
const cChoice = p.get("choice");
const cRem = p.get("remember") || "no";
const rest = p.get("resturl") || "/";
if (!target) return;
if (cChoice === "bcnr" || cChoice === "icann") {
rememberCollision(target, cTld, cChoice, cRem);
tab.collisionOverride = cChoice; // one-shot, consumed by loadBns
}
return navigateTab(id, target + rest);
}
if (parsed.protocol === "bns:") return;
if (isBnsHost(parsed.hostname)) { e.preventDefault(); navigateTab(id, parsed.hostname + parsed.pathname); }
} catch {}
@ -901,7 +898,10 @@ async function loadBns(t, id, host, rest, tld) {
if (id === activeId) pushNav({ host, kind: "resolving", tld, registry });
const fallbackToWeb = async (reason) => {
t.url = "https://" + host + (rest === "/" ? "" : rest);
try { await t.view.webContents.loadURL(t.url); } catch {}
t.internalNav = true;
try { await t.view.webContents.loadURL(t.url); }
catch (e) { console.warn("fallback loadURL failed:", e?.message); }
finally { t.internalNav = false; }
t.prov = { host, kind: "web", note: `fallback: ${reason}`, tld, registry };
if (id === activeId) pushNav(t.prov);
emitTabs();