theseus/error.html
Local Dev aea158ab05 Theseus 0.3.122 followups: AI engines page, Pick-AI edit button, Pithos panel, Did-you-mean on DNS failures, LLM picker UI polish, real-brand icon preference
Shipped on top of the 0.3.122 Settings restructure commit.

AI engines settings page. New top-level nav entry "AI engines" with its own
Default picker, Enabled list, catalog, discover filter and + Add AI engine
form. The Search page is now LLM-free — ENGINE_KINDS is `["search"]`, the
Search catalog filters out kind="llm" entries, and the Search custom-URL
form only adds kind="search" engines. add-engine IPC now accepts a kind:
"llm" field (sets defaultLlmId instead of searchEngine for LLM customs).

Pick-AI picker. Borders removed, corners rounded (16px), heavier shadow;
"Pick AI engines…" footer row dropped. Edit button (pencil icon) added to
the header; opens the new AI engines settings page via `llm-picker-settings`,
which now routes to the "ai-engines" slug (openSettingsTab falls through to
"search" for legacy profiles that lack the section).

Icon priority flipped. allEngines() now prefers a fetched favicon (the
site's own brand mark, cached per profile) over the bundled generic tile.
Bundled tiles still carry the first paint while the fetch runs, and the
emit after fetch swaps in the real brand. Offline profiles see the tile
indefinitely — same reliability as before.

Pithos sidebar panel. bundled-addons/pithos/addon.json declares a `panels`
block so sidebarLeftPanels() returns Pithos's panel BEFORE the add-on's
on-demand activation. Previously, the Sidebar settings' Theseus native
apps card was missing Pithos: it only appeared after first launch, when
the add-on's own registerSidebarPanel call ran. Hermes already did this.

Did-you-mean on DNS failures. The error page's "Did you mean" row now
merges BCNR warm-index matches with ICANN alternative-TLD swaps + history
base-name matches. For my.mynymbox.com the suggestions are my.mynymbox.io,
mynymbox.io, my.mynymbox.net, mynymbox.net — the subdomain-strip is
interleaved so a mistake like typing my.mynymbox.com when the real site
is client.mynymbox.io still surfaces mynymbox.io near the top. Both paths
are local-only; no network, no leak of what the user is browsing.

Sidebar settings tightened. "How panels open" collapsed to one row with
two side-by-side option cards; "Reset panel widths" and "AI engine in the
sidebar" are one-liners now.
2026-10-27 20:50:40 +02:00

311 lines
15 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Load error</title>
<style>
:root { color-scheme: light dark;
--bg:#0b0e14; --panel:#141a24; --panel2:#18202c; --line:rgba(255,255,255,.09);
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d;
--warn:#ffc75f; --err:#f6768a; --info:#4b7bec; }
@media (prefers-color-scheme: light) {
:root { --bg:#f6f8fb; --panel:#ffffff; --panel2:#f0f4f9; --line:rgba(0,0,0,.10);
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5;
/* Darker acid for light backgrounds — ~5.5:1 on white. */
--acid: #0AC18E; }
}
* { box-sizing: border-box }
html, body { margin: 0; height: 100% }
body {
background: radial-gradient(1100px 560px at 50% -12%, #16202e, var(--bg));
color: var(--ink); font: 15px/1.6 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
display: grid; place-items: center; padding: 2rem 1.2rem;
}
.card { max-width: 640px; width: 100%; background: var(--panel); border: 1px solid var(--line);
border-radius: 14px; padding: 2rem 2.2rem 1.7rem; }
.head { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 1rem; }
.icon { width: 42px; height: 42px; flex: none; border-radius: 10px; display: grid; place-items: center;
font-size: 22px; background: rgba(246,118,138,.10); }
.icon.warn { background: rgba(255,199,95,.10) }
.icon.info { background: rgba(75,123,236,.10) }
h1 { font-size: 1.15rem; font-weight: 600; margin: .1rem 0 .1rem; letter-spacing: .1px }
.sub { color: var(--mut); font-size: 13.5px; margin: 0; }
.body { color: var(--ink); font-size: 14px; line-height: 1.62; margin: 1rem 0 .6rem; }
.body b { color: var(--acid); }
.body .host { font-family: ui-monospace, monospace; background: var(--panel2); padding: 1px 6px;
border-radius: 5px; color: var(--acid); word-break: break-all; }
.tried { margin: 1rem 0 0; padding: 12px 14px; background: var(--panel2);
border: 1px dashed var(--line); border-radius: 10px; font-size: 12.5px; color: var(--mut); }
.tried .t { display: flex; gap: 8px; align-items: center; padding: 3px 0; }
.tried .t .m { width: 12px; text-align: center; color: var(--err); }
.tried .t .m.ok { color: var(--acid) }
.suggest { margin: 1rem 0 0; padding: 12px 14px; background: var(--panel2);
border: 1px solid rgb(from var(--acid) r g b / .25); border-radius: 10px; font-size: 13px; color: var(--ink); }
.suggest .lbl { color: var(--mut); font-size: 12px; margin-bottom: 6px; }
.suggest .row { display: flex; flex-wrap: wrap; gap: 6px 8px; }
.suggest a { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 6px;
background: var(--panel); border: 1px solid var(--line); color: var(--acid);
font-family: ui-monospace, monospace; font-size: 12.5px; text-decoration: none; cursor: pointer; }
.suggest a:hover { border-color: rgb(from var(--acid) r g b / .55); filter: brightness(1.05); }
.actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 1.3rem; }
.btn { font-family: inherit; font-size: 13.5px; padding: 9px 16px; border-radius: 9px;
border: 1px solid var(--line); background: var(--panel2); color: var(--ink);
cursor: pointer; transition: filter .1s, background .1s, border-color .1s; }
.btn:hover { background: var(--panel); border-color: rgb(from var(--acid) r g b / .35); filter: brightness(1.05); }
.btn.primary { background: var(--acid); color: #0b0e14; border-color: transparent; font-weight: 600; }
.btn.primary:hover { background: var(--acid); filter: brightness(1.08); }
.details { margin-top: 1.2rem; padding-top: .9rem; border-top: 1px solid var(--line);
font: 11.5px/1.55 ui-monospace, monospace; color: var(--dim); }
.details .row { display: grid; grid-template-columns: 78px 1fr; gap: 8px; }
.details .row b { color: var(--mut); font-weight: 500; }
</style>
</head>
<body>
<div class="card">
<div class="head">
<div class="icon" id="icon">✕</div>
<div>
<h1 id="title">Couldn't load this page</h1>
<p class="sub" id="sub">The tab kept its address so you can edit and try again.</p>
</div>
</div>
<div class="body" id="body"></div>
<div class="tried" id="tried" hidden>
<div class="t"><span class="m">✕</span> <span>Bitcoin Cash Domain Names (BCDN) — no record on chain</span></div>
<div class="t"><span class="m">✕</span> <span>Clearnet DNS — no host at this name either</span></div>
</div>
<div class="suggest" id="suggest" hidden>
<div class="lbl">Did you mean</div>
<div class="row" id="suggestRow"></div>
</div>
<div class="actions" id="actions"></div>
<div class="details">
<div class="row"><b>URL</b><span id="dUrl"></span></div>
<div class="row"><b>Error</b><span id="dErr"></span></div>
</div>
</div>
<script>
const q = new URLSearchParams(location.search);
let kind = q.get("kind") || "generic"; // offline | name-not-registered | name-unreachable | unreachable | tls | generic
const host = q.get("host") || "";
const url = q.get("url") || "";
const code = q.get("code") || "";
const desc = q.get("desc") || "";
// Offline is a special case: if the browser process itself has no
// network, every load will fail here and we'd otherwise mis-report
// BCNR-eligible names as "not registered" (we couldn't reach the
// beacon to know). Detect it up front so the user sees a clear
// "you're offline" instead of a false verdict about the name.
if (typeof navigator !== "undefined" && navigator.onLine === false) kind = "offline";
const $ = (id) => document.getElementById(id);
const el = (tag, cls, txt) => { const e = document.createElement(tag); if (cls) e.className = cls; if (txt != null) e.textContent = txt; return e; };
const hostSpan = (h) => { const s = el("span", "host"); s.textContent = h || "(no host)"; return s; };
document.title = host ? "Error — " + host : "Load error";
$("dUrl").textContent = url || "(none)";
$("dErr").textContent = (code ? code + " " : "") + (desc || "");
// Kind → title, subtitle, body, actions.
const configs = {
"offline": {
icon: "⌀", iconCls: "info",
title: "You appear to be offline",
sub: "No network connection detected",
body: (b) => {
b.append("This browser can't reach the network. Wi-Fi may be off, the cable may be unplugged, or a VPN / proxy may be dropping traffic. ");
b.append("Reconnect and press ");
const kbd = document.createElement("b"); kbd.textContent = "Retry"; b.append(kbd);
b.append(".");
if (host) {
b.append(document.createElement("br"));
b.append(document.createElement("br"));
b.append("Trying to open ");
b.append(hostSpan(host));
b.append(" — the address is kept so you can retry once you're back online.");
}
},
actions: ["retry", "home"],
},
"name-not-registered": {
icon: "?", iconCls: "warn",
title: "That name isn't registered",
sub: "No BCDN record on chain, and no clearnet host at " + host,
body: (b) => {
b.append("Nothing answered when we asked for ");
b.append(hostSpan(host));
b.append(". Bitcoin Cash Domain Names had no record for it, and ordinary DNS has no host at that name either.");
b.append(document.createElement("br"));
b.append(document.createElement("br"));
b.append("If it's supposed to be yours, register it on the chain. If you were guessing, try a search instead.");
},
tried: true,
actions: ["register", "search", "retry", "home"],
},
"name-unreachable": {
icon: "⌘", iconCls: "warn",
title: "This name didn't resolve",
sub: "Your DNS resolver didn't know " + host,
body: (b) => {
b.append("We couldn't turn ");
b.append(hostSpan(host));
b.append(" into an address. Your network's resolver may be blocking it, offline, or the name simply doesn't exist. BCDN was tried first — if the host is a BCDN name, it's not registered on the chain either.");
},
actions: ["retry", "search", "register", "home"],
},
"unreachable": {
icon: "✕", iconCls: "",
title: "This site is unreachable",
sub: host ? ("Couldn't connect to " + host) : "The server didn't respond",
body: (b) => {
b.append("The name resolved, but the server at ");
b.append(hostSpan(host));
b.append(" didn't answer. It may be down, blocking your network, or reachable only through Tor.");
},
actions: ["retry", "tor-hint", "home"],
},
"tls": {
icon: "!", iconCls: "warn",
title: "The site's certificate isn't trustworthy",
sub: host ? ("HTTPS handshake with " + host + " failed") : "Certificate error",
body: (b) => {
b.append("The server at ");
b.append(hostSpan(host));
b.append(" presented a certificate the browser couldn't verify. This can mean the certificate has expired, doesn't match the hostname, was self-signed, or that someone is intercepting the connection.");
},
actions: ["retry", "home"],
},
"cf-blocked": {
icon: "⛔", iconCls: "warn",
title: "This site blocks Electron browsers",
sub: host ? (host + " returned 503 — Cloudflare Bot Fight Mode") : "Cloudflare Bot Fight Mode",
body: (b) => {
b.append("The server at ");
b.append(hostSpan(host));
b.append(" is behind Cloudflare's Bot Fight Mode. Cloudflare fingerprints the browser's TLS handshake below HTTP and refuses Electron-based browsers like Theseus, no matter what user-agent is sent.");
b.append(document.createElement("br"));
b.append(document.createElement("br"));
const mirror = q.get("mirror");
if (mirror) {
b.append("A ");
const s = el("b", null, ".bch mirror");
b.append(s);
b.append(" is registered on chain for this site: ");
const a = el("a", "host", mirror);
a.href = "#";
a.style.cursor = "pointer";
a.addEventListener("click", (ev) => {
ev.preventDefault();
// Preserve the original path/search/hash so /faq on the upstream
// becomes /faq on the mirror, not the mirror's root.
let origPath = "/";
try { const u = new URL(url); origPath = u.pathname + u.search + u.hash; } catch {}
window.errorpage.retry("https://" + mirror + origPath);
});
b.append(a);
b.append(". Theseus proxies the request through Node's HTTP stack, which Cloudflare doesn't block — the page will load.");
} else {
b.append("No .bch mirror is registered for this site yet. See ");
const a = el("a", "host", "silentmode.st/mirrors");
a.href = "#";
a.style.cursor = "pointer";
a.addEventListener("click", (ev) => {
ev.preventDefault();
window.errorpage.openExternal("https://silentmode.st/mirrors/");
});
b.append(a);
b.append(" — it explains why this happens and how to mint one.");
}
},
actions: ["retry", "home"],
},
"generic": {
icon: "✕", iconCls: "",
title: "Couldn't load this page",
sub: host ? ("Something went wrong loading " + host) : "Load failed",
body: (b) => {
b.append("Something interrupted the load of ");
b.append(hostSpan(host));
b.append(". Details are at the bottom of this page.");
},
actions: ["retry", "home"],
},
};
const c = configs[kind] || configs.generic;
$("icon").textContent = c.icon;
$("icon").className = "icon" + (c.iconCls ? " " + c.iconCls : "");
$("title").textContent = c.title;
$("sub").textContent = c.sub;
c.body($("body"));
if (c.tried) $("tried").hidden = false;
// Actions — order matters (first is primary). "register" only when we can
// construct a Sirius URL (need a host).
const btnDefs = {
retry: { label: "Retry", primary: true, fn: () => window.errorpage.retry(url) },
search: { label: "Search for '" + host + "'", primary: false, fn: () => window.errorpage.searchFor(host) },
register: { label: "Register on Sirius", primary: false, fn: () => window.errorpage.registerOnSirius(host) },
home: { label: "Home", primary: false, fn: () => window.errorpage.goHome() },
"tor-hint": {
label: "Open Tor guide", primary: false,
fn: () => window.errorpage.openExternal("https://silentmode.st/tools/#tor"),
},
};
const actionsBox = $("actions");
let primaryUsed = false;
for (const a of c.actions) {
const def = btnDefs[a]; if (!def) continue;
if (a === "register" && !host) continue;
if (a === "search" && !host) continue;
const b = el("button", "btn" + (def.primary && !primaryUsed ? " primary" : ""), def.label);
if (def.primary) primaryUsed = true;
b.addEventListener("click", def.fn);
actionsBox.appendChild(b);
}
// "Did you mean" — two sources, merged into one row:
// • BCNR warm shared index (Levenshtein ≤ 2, same TLD) — for a BCDN name
// typo like "games.x" → "game.x". Local index; works offline.
// • ICANN alternative-TLD swaps + history base-name matches — a DNS
// failure on my.mynymbox.com surfaces my.mynymbox.io, .net, .org, …,
// and any related host the user has visited. Local too.
// Clicking a suggestion retries at that host, preserving the original path.
if ((kind === "name-not-registered" || kind === "name-unreachable") && host) {
let origPath = "/";
try { const u = new URL(url); origPath = u.pathname + u.search + u.hash; } catch {}
const sources = [
window.errorpage?.bnsSimilar ? window.errorpage.bnsSimilar(host) : Promise.resolve([]),
window.errorpage?.icannAlternatives ? window.errorpage.icannAlternatives(host) : Promise.resolve([]),
];
Promise.all(sources).then(([bns, icann]) => {
const seen = new Set();
const matches = [];
for (const name of [...(bns || []), ...(icann || [])]) {
if (!name || seen.has(name)) continue;
seen.add(name); matches.push(name);
if (matches.length >= 4) break;
}
if (!matches.length) return;
const row = $("suggestRow");
for (const name of matches) {
const a = el("a", null, name);
a.href = "#";
a.addEventListener("click", (ev) => {
ev.preventDefault();
window.errorpage.retry("https://" + name + origPath);
});
row.appendChild(a);
}
$("suggest").hidden = false;
}).catch(() => {});
}
</script>
</body>
</html>