Theseus navigator UI overhaul, BNS-only indexer, site restructure

Theseus Navigator:
- Firefox-style toolbar: SVG back/fwd/reload/home buttons
- security padlock badge in the address bar opens a floating site-info
  overlay VIEW (on top of the page, never pushes content down)
- Edge-style favorites bar (shown only on the new-tab/home page)
- search-engine picker (DuckDuckGo/Google/Bing/Brave/Startpage)
- Theseus logo moved right, opens Settings; native menu removed
- address bar doubles as search; bottom resolver status line

Argus:
- BNS-only indexer: an electrum endpoint over a bare BCHN node
  (watches just the beacon, caches txs so no txindex needed) + test
- registry lifecycle / portfolio / name-index modules

Site restructure (nav/apps/store/hermes/choose) + runbook notes.
This commit is contained in:
Local Dev 2026-07-30 08:16:55 +02:00
parent 7898e78ff4
commit 89c80a0103
5 changed files with 264 additions and 100 deletions

View file

@ -3,6 +3,8 @@
<head><meta charset="utf-8">
<style>
:root { color-scheme: dark; font-family: system-ui, sans-serif; }
/* `display:flex/grid` otherwise overrides the HTML hidden attribute — force it. */
[hidden] { display: none !important; }
body { margin: 0; height: 100vh; background: #0f1420; color: #e7eaf1; user-select: none; overflow: hidden; }
/* tab strip */
.tabs { display: flex; align-items: flex-end; gap: 4px; padding: 6px 8px 0; height: 34px; }
@ -18,7 +20,6 @@
/* toolbar */
.bar { display: flex; gap: 6px; align-items: center; padding: 6px 10px; }
.nav { display: flex; gap: 1px; }
/* Firefox-style round-rect icon buttons: 16px stroke glyph in a 34x32 hit target */
.ic { width: 34px; height: 32px; display: grid; place-items: center; border-radius: 8px; cursor: pointer;
color: #c5cdda; background: transparent; border: none; padding: 0; }
.ic svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.7;
@ -26,41 +27,51 @@
.ic:hover { background: #ffffff14; color: #fff; }
.ic:active { background: #ffffff22; }
.ic:disabled { opacity: .28; cursor: default; background: transparent; }
/* clickable Theseus logo = site-info button */
.logo { display: flex; align-items: center; gap: 6px; font-weight: 700; color: #d6ff3d; white-space: nowrap;
padding: 5px 10px; margin: 0 2px; border: 1px solid transparent; border-radius: 8px; cursor: pointer;
background: transparent; font-size: 13px; }
.logo:hover { background: rgba(214,255,61,.10); border-color: #d6ff3d33; }
.logo.open { background: rgba(214,255,61,.14); border-color: #d6ff3d55; }
.logo .chev { color: #9fb04e; font-size: 9px; }
input { padding: 8px 13px; border-radius: 999px; border: 1px solid #ffffff22; background: #1b2330; color: inherit; font-size: 13.5px; outline: none; }
input:focus { border-color: #4b7bec; }
.urlwrap { flex: 1; display: flex; align-items: center; gap: 6px; background: #1b2330; border: 1px solid #ffffff22;
border-radius: 999px; padding-right: 8px; }
/* address bar with a security badge on the left + bookmark star on the right */
.urlwrap { flex: 1; display: flex; align-items: center; gap: 4px; background: #1b2330; border: 1px solid #ffffff22;
border-radius: 999px; padding: 0 6px 0 4px; }
.urlwrap:focus-within { border-color: #4b7bec; }
#url { flex: 1; border: none; background: transparent; border-radius: 999px; }
#url:focus { border: none; }
#search { width: 180px; }
/* Firefox-style padlock badge */
.secbadge { display: grid; place-items: center; width: 28px; height: 26px; border-radius: 6px; border: none;
background: transparent; cursor: pointer; padding: 0; }
.secbadge:hover { background: #ffffff14; }
.secbadge svg { width: 14px; height: 14px; }
.secbadge svg .shk { fill: none; stroke: #a9b3c2; stroke-width: 1.3; }
.secbadge svg .bdy { fill: #a9b3c2; }
.secbadge.warn svg .shk { stroke: #f6ad55; } .secbadge.warn svg .bdy { fill: #f6ad55; }
.star { border: none; background: transparent; cursor: pointer; color: #8b98a9; font-size: 15px; padding: 2px 4px; border-radius: 6px; }
.star:hover { background: #ffffff14; color: #fff; } .star.on { color: #ffd23d; }
input { padding: 7px 6px; border-radius: 999px; border: none; background: transparent; color: inherit; font-size: 13.5px; outline: none; }
#url { flex: 1; }
/* search box + engine picker */
.searchbox { display: flex; align-items: center; gap: 2px; background: #1b2330; border: 1px solid #ffffff22;
border-radius: 999px; padding-left: 8px; }
.searchbox:focus-within { border-color: #4b7bec; }
.searchbox .mag { color: #8b98a9; font-size: 12px; }
#search { width: 150px; }
.engine { background: #10151f; color: #b9c2d0; border: none; border-left: 1px solid #ffffff1a; border-radius: 0 999px 999px 0;
font-size: 11.5px; padding: 6px 6px; outline: none; cursor: pointer; }
.reg { font-size: 10.5px; letter-spacing: .03em; padding: 2px 8px; border-radius: 999px; white-space: nowrap;
background: rgba(214,255,61,.13); color: #d6ff3d; border: 1px solid #d6ff3d33; }
.tor { padding: 7px 10px; border-radius: 8px; border: 1px solid #ffffff22; background: #2b2f3a; color: #fff; cursor: pointer; white-space: nowrap; font-size: 12.5px; }
.tor.connecting { background: #7a5c14; } .tor.on { background: #6b3fa0; }
.sep { width: 1px; height: 22px; background: #ffffff1a; margin: 0 4px; }
.searchbox { display: flex; align-items: center; gap: 4px; }
.searchbox .mag { color: #8b98a9; font-size: 13px; }
/* registry/TLD badge — trusted chrome, painted by the browser not the page */
.reg { font-size: 10.5px; letter-spacing: .03em; padding: 2px 9px; border-radius: 999px;
background: rgba(214,255,61,.13); color: #d6ff3d; border: 1px solid #d6ff3d33; white-space: nowrap; }
/* site-info dropdown panel (opened by the logo) */
.siteinfo { font-size: 12px; color: #dbe3ef; background: #141b28; border-top: 1px solid #ffffff12;
padding: 10px 16px; display: grid; grid-template-columns: auto 1fr; gap: 5px 14px; }
.siteinfo .k { color: #7f8aa0; }
.siteinfo .v { color: #e7eaf1; }
.siteinfo .v code { font-family: ui-monospace, monospace; color: #bfeae4; }
.siteinfo .dotv { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #5e6678; margin-right: 6px; vertical-align: middle; }
.siteinfo .dotv.chain { background: #d6ff3d; } .siteinfo .dotv.sia { background: #b39ddb; }
.siteinfo .dotv.server { background: #4fd1a5; } .siteinfo .dotv.web { background: #8b93a7; } .siteinfo .dotv.err { background: #f6768a; }
/* Theseus logo — moved to the right, opens Settings */
.logo { display: flex; align-items: center; gap: 6px; font-weight: 700; color: #d6ff3d; white-space: nowrap;
padding: 6px 12px; border: 1px solid #d6ff3d33; border-radius: 8px; cursor: pointer;
background: rgba(214,255,61,.08); font-size: 13px; }
.logo:hover { background: rgba(214,255,61,.16); }
.logo .gear { font-size: 12px; opacity: .8; }
/* bookmarks / saved-pages bar (Edge-style) */
.bookmarks { display: flex; align-items: center; gap: 4px; padding: 3px 10px 5px; overflow: hidden; }
.bm { display: flex; align-items: center; gap: 6px; max-width: 180px; padding: 3px 9px; border-radius: 6px;
background: #171e2a; border: 1px solid #ffffff10; color: #c5cdda; font-size: 12px; cursor: pointer; }
.bm:hover { background: #202a3a; color: #fff; }
.bm .bt { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bm .bx { opacity: 0; cursor: pointer; font-size: 11px; } .bm:hover .bx { opacity: .55; }
.bm .bx:hover { opacity: 1; color: #f6768a; }
.bm-empty { color: #5e6678; font-size: 11.5px; padding: 3px 4px; }
.tordisc { font-size: 11.5px; color: #d9c7f2; background: #2a1c40; border-top: 1px solid #6b3fa055; padding: 5px 14px; }
.tordisc a { color: #d6ff3d; }
/* passive "also on BCNR" bar — dual-use TLD switch (never hijacks the web) */
.bcnrbar { display: flex; align-items: center; gap: 10px; font-size: 11.5px; color: #dbead0;
background: #1a2417; border-top: 1px solid #d6ff3d33; padding: 6px 14px; }
.bcnrbar .breg { font-size: 10px; letter-spacing: .03em; padding: 1px 7px; border-radius: 999px;
@ -69,38 +80,34 @@
.bcnrbar .bopen { margin-left: auto; padding: 4px 11px; border-radius: 7px; border: 1px solid #d6ff3d55;
background: rgba(214,255,61,.12); color: #eaffb0; cursor: pointer; font-size: 11.5px; }
.bcnrbar .bopen:hover { background: rgba(214,255,61,.22); }
.bcnrbar .bdismiss { color: #8b98a9; text-decoration: none; }
.bcnrbar .bdismiss:hover { color: #fff; }
.bcnrbar .bdismiss { color: #8b98a9; text-decoration: none; } .bcnrbar .bdismiss:hover { color: #fff; }
</style></head>
<body>
<div class="tabs" id="tabs"></div>
<div class="bar">
<div class="nav">
<button class="ic" id="back" title="Back">
<svg viewBox="0 0 16 16"><path d="M10 3 L5 8 L10 13"/></svg></button>
<button class="ic" id="fwd" title="Forward">
<svg viewBox="0 0 16 16"><path d="M6 3 L11 8 L6 13"/></svg></button>
<button class="ic" id="reload" title="Reload">
<svg viewBox="0 0 16 16"><path d="M13 8 a5 5 0 1 1 -1.5 -3.6"/><path d="M13 2.5 L13 5 L10.5 5"/></svg></button>
<button class="ic" id="home" title="Home">
<svg viewBox="0 0 16 16"><path d="M2.5 7.5 L8 3 L13.5 7.5"/><path d="M4 6.7 L4 13 L12 13 L12 6.7"/></svg></button>
<button class="ic" id="back" title="Back"><svg viewBox="0 0 16 16"><path d="M10 3 L5 8 L10 13"/></svg></button>
<button class="ic" id="fwd" title="Forward"><svg viewBox="0 0 16 16"><path d="M6 3 L11 8 L6 13"/></svg></button>
<button class="ic" id="reload" title="Reload"><svg viewBox="0 0 16 16"><path d="M13 8 a5 5 0 1 1 -1.5 -3.6"/><path d="M13 2.5 L13 5 L10.5 5"/></svg></button>
<button class="ic" id="home" title="Home"><svg viewBox="0 0 16 16"><path d="M2.5 7.5 L8 3 L13.5 7.5"/><path d="M4 6.7 L4 13 L12 13 L12 6.7"/></svg></button>
</div>
<button class="logo" id="logo" title="Site information">
<span>⛓ Theseus</span><span class="chev"></span></button>
<div class="urlwrap">
<button class="secbadge" id="secbadge" title="Site information" hidden>
<svg viewBox="0 0 16 16" aria-hidden="true"><path class="shk" d="M5 7 V5.3 a3 3 0 0 1 6 0 V7"/><rect class="bdy" x="3.6" y="6.9" width="8.8" height="6" rx="1.2"/></svg>
</button>
<input id="url" placeholder="Search or enter a .bch / .p2p name or web address" spellcheck="false">
<span class="reg" id="reg" hidden></span>
<button class="star" id="star" title="Save this page"></button>
</div>
<span class="sep"></span>
<div class="searchbox">
<span class="mag">🔍</span>
<input id="search" placeholder="Search the web" spellcheck="false">
<input id="search" placeholder="Search" spellcheck="false">
<select id="engine" class="engine" title="Search engine"></select>
</div>
<button class="tor" id="tor" title="Route traffic through Tor">🧅 Tor: Off</button>
<button class="ic" id="settings" title="Settings">
<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="2.2"/><path d="M8 1.5 L8 3 M8 13 L8 14.5 M1.5 8 L3 8 M13 8 L14.5 8 M3.3 3.3 L4.3 4.3 M11.7 11.7 L12.7 12.7 M12.7 3.3 L11.7 4.3 M4.3 11.7 L3.3 12.7"/></svg></button>
<button class="logo" id="logo" title="Settings"><span>⛓ Theseus</span><span class="gear"></span></button>
</div>
<div id="siteinfo" class="siteinfo" hidden></div>
<div class="bookmarks" id="bookmarks" hidden></div>
<div id="tordisc" class="tordisc" hidden>
Onion mode hides your IP from sites and your ISP (including .bch lookups).
It is <b>not full anonymity</b> — this browser can still be fingerprinted; for that use the Tor Browser.
@ -115,8 +122,9 @@
<script>
const $ = (id) => document.getElementById(id);
const T = window.theseus;
// Keep the chrome view tall enough for whatever bars are visible (site info / Tor / BCNR).
function syncHeight() { requestAnimationFrame(() => T.setChromeHeight(document.body.scrollHeight)); }
// ---- navigation ----
function goURL() { const v = $("url").value.trim(); if (v) T.navigate(v); }
$("url").addEventListener("keydown", (e) => { if (e.key === "Enter") goURL(); });
$("search").addEventListener("keydown", (e) => { if (e.key === "Enter") { const q = $("search").value.trim(); if (q) { T.search(q); $("search").value = ""; } } });
@ -124,59 +132,76 @@
$("fwd").onclick = () => T.forward();
$("reload").onclick = () => T.reload();
$("home").onclick = () => T.goHome();
$("settings").onclick = () => T.openSettings();
$("logo").onclick = () => T.openSettings(); // logo (right) opens Settings
// ---- site-info panel (toggled by the Theseus logo) ----
let lastNav = null, infoOpen = false;
function renderInfo() {
const d = lastNav, box = $("siteinfo");
const row = (k, v) => `<span class="k">${k}</span><span class="v">${v}</span>`;
let html = "";
if (!d || d.kind === "home") {
html = row("Page", "Theseus home") +
row("About", "Theseus is a decentralized-web navigator. It resolves BCNR names (.bch, .p2p, .nav…) straight from the Bitcoin&nbsp;Cash chain — no ICANN, no DNS middleman.");
} else if (d.kind === "web") {
html = row("Site", `<span class="dotv web"></span><code>${(d.host||"").replace(/</g,"&lt;")}</code>`) +
row("Resolved via", "ICANN DNS (ordinary web)") +
row("Registry", "ICANN — not a BCNR name");
} else if (d.kind === "resolving") {
html = row("Site", `<code>${(d.host||"").replace(/</g,"&lt;")}</code>`) + row("Status", "resolving on the BCH chain…");
} else if (d.kind === "nxdomain") {
html = row("Name", `<span class="dotv err"></span><code>${(d.host||"").replace(/</g,"&lt;")}</code>`) +
row("Status", "not registered on the BCNR chain (NXDOMAIN)");
} else if (d.kind === "error") {
html = row("Status", `<span class="dotv err"></span>${(d.error||"resolution failed").replace(/</g,"&lt;")}`);
} else { // ok
const cls = (d.source||"").includes("chain") ? "chain" : (d.source||"").includes("Sia") ? "sia" : (d.source||"").includes("server") ? "server" : "web";
html = row("Site", `<span class="dotv ${cls}"></span><code>${(d.host||"").replace(/</g,"&lt;")}</code>`) +
row("Served from", d.source || "chain") +
row("Registry", (d.registry || "BCNR") + (d.tld ? " · ." + d.tld : "")) +
(d.records && d.records.length ? row("Records", `<code>${d.records.join(", ")}</code>`) : "") +
(d.category ? row("Certificate", `<code>${String(d.category).slice(0,24)}…</code>`) : "");
}
box.innerHTML = html;
}
function toggleInfo(force) {
infoOpen = force !== undefined ? force : !infoOpen;
$("siteinfo").hidden = !infoOpen;
$("logo").classList.toggle("open", infoOpen);
if (infoOpen) renderInfo();
// ---- search engine picker ----
T.getSearchEngines().then((d) => {
const sel = $("engine");
sel.innerHTML = d.engines.map((e) => `<option value="${e.id}">${e.name}</option>`).join("");
sel.value = d.current;
sel.onchange = () => T.setSearchEngine(sel.value);
});
// ---- bookmarks bar ----
let current = { url: "", title: "" };
let bookmarks = [];
function renderBookmarks() {
const box = $("bookmarks");
if (!bookmarks.length) { box.innerHTML = `<span class="bm-empty">No saved pages yet — click ☆ to save the current page</span>`; syncHeight(); return; }
box.innerHTML = bookmarks.map((b, i) =>
`<div class="bm" data-i="${i}" title="${(b.url || "").replace(/"/g, "&quot;")}">
<span class="bt">${(b.title || b.url).replace(/</g, "&lt;")}</span>
<span class="bx" data-x="${i}"></span></div>`).join("");
box.querySelectorAll(".bm").forEach((el) => el.onclick = (e) => {
const i = Number(el.dataset.i);
if (e.target.dataset.x !== undefined) { T.removeBookmark(bookmarks[i].url); }
else T.navigate(bookmarks[i].url);
});
syncHeight();
updateStar();
}
$("logo").onclick = () => toggleInfo();
function updateStar() {
const saved = bookmarks.some((b) => b.url === current.url && current.url);
const s = $("star"); s.textContent = saved ? "★" : "☆"; s.classList.toggle("on", saved);
s.title = saved ? "Remove from saved pages" : "Save this page";
}
$("star").onclick = () => {
if (!current.url) return;
if (bookmarks.some((b) => b.url === current.url)) T.removeBookmark(current.url);
else T.addBookmark({ title: current.title || current.url, url: current.url });
};
T.getBookmarks().then((b) => { bookmarks = b || []; renderBookmarks(); });
T.onBookmarks((b) => { bookmarks = b || []; renderBookmarks(); });
// ---- security badge (Firefox-style padlock); opens the floating overlay ----
function setBadge(d) {
const b = $("secbadge");
if (!d || d.kind === "home") { b.hidden = true; return; }
b.hidden = false;
let cls = "secbadge", title = "Site information — click for details";
if (d.kind === "ok") title = "Secure · " + (d.source || "BCNR") + " — click for details";
else if (d.kind === "web") title = "Connection — click for details";
else if (d.kind === "resolving") title = "Resolving…";
else if (d.kind === "nxdomain") { cls = "secbadge warn"; title = "Not registered on BCNR"; }
else if (d.kind === "error") { cls = "secbadge warn"; title = "Resolution error"; }
b.className = cls; b.title = title;
}
// The popover is a separate overlay view; hand main the badge's screen position.
$("secbadge").onclick = () => {
const r = $("secbadge").getBoundingClientRect();
T.toggleSiteInfo({ x: Math.round(r.left), y: Math.round(r.bottom + 4) });
};
// ---- Tor ----
let torShown = false;
$("tor").onclick = () => { T.toggleTor(); if (!torShown) { $("tordisc").hidden = false; torShown = true; syncHeight(); } };
$("tordismiss").onclick = (e) => { e.preventDefault(); $("tordisc").hidden = true; syncHeight(); };
// Passive "also on BCNR" bar for dual-use ICANN TLDs (.de/.dev/.ltd).
// ---- passive BCNR offer ----
T.onBcnrOffer((d) => {
const bar = $("bcnrbar");
if (d && d.host) {
$("bcnrhost").textContent = d.host;
$("bcnrreg").textContent = d.registry || "BCNR";
bar.hidden = false;
} else { bar.hidden = true; }
if (d && d.host) { $("bcnrhost").textContent = d.host; $("bcnrreg").textContent = d.registry || "BCNR"; bar.hidden = false; }
else bar.hidden = true;
syncHeight();
});
$("bcnropen").onclick = () => { $("bcnrbar").hidden = true; syncHeight(); T.switchToBcnr(); };
@ -186,9 +211,14 @@
b.textContent = d.state === "on" ? "🧅 Tor: On" : d.state === "connecting" ? "🧅 connecting…" : "🧅 Tor: Off";
});
// ---- tabs ----
T.onTabs((d) => {
$("back").disabled = !d.canBack; $("fwd").disabled = !d.canForward;
if (document.activeElement !== $("url")) $("url").value = d.url || "";
current.url = d.url || "";
const active = d.tabs.find((t) => t.active);
current.title = active ? active.title : "";
updateStar();
const box = $("tabs");
box.innerHTML = d.tabs.map((t) =>
`<div class="tab ${t.active ? "active" : ""}" data-id="${t.id}">
@ -202,18 +232,19 @@
$("newtab").onclick = () => T.newTab();
});
// ---- provenance ----
T.onNav((d) => {
lastNav = d;
// Registry/TLD badge: shown for any BNS name, painted here (trusted chrome).
setBadge(d);
const reg = $("reg");
if (d.tld) { reg.hidden = false; reg.textContent = (d.registry || "BCNR") + " · ." + d.tld; }
else reg.hidden = true;
// Keep the address bar in sync (unless the user is typing).
if (d.tld) { reg.hidden = false; reg.textContent = (d.registry || "BCNR") + " · ." + d.tld; } else reg.hidden = true;
if (document.activeElement !== $("url")) {
if (d.kind === "home") $("url").value = "";
else if (d.host && !$("url").value) $("url").value = d.host;
}
if (infoOpen) renderInfo();
// Favorites bar shows only on the new-tab / home page (Edge-style); it hides
// once a real page loads so it never eats space while browsing.
$("bookmarks").hidden = !(!d || d.kind === "home");
syncHeight();
});
syncHeight();

78
main.js
View file

@ -21,7 +21,15 @@ const RES_DIR = app.isPackaged ? process.resourcesPath : __dirname;
const RESOLVER = app.isPackaged
? path.join(RES_DIR, "resolver-web.mjs")
: path.join(__dirname, "..", "Argus", "src", "lib", "resolver-web.js");
const SEARCH = (q) => "https://duckduckgo.com/?q=" + encodeURIComponent(q);
// Search engines the user can pick from (default persisted in settings.searchEngine).
const SEARCH_ENGINES = {
duckduckgo: { name: "DuckDuckGo", url: (q) => "https://duckduckgo.com/?q=" + encodeURIComponent(q) },
google: { name: "Google", url: (q) => "https://www.google.com/search?q=" + encodeURIComponent(q) },
bing: { name: "Bing", url: (q) => "https://www.bing.com/search?q=" + encodeURIComponent(q) },
brave: { name: "Brave", url: (q) => "https://search.brave.com/search?q=" + encodeURIComponent(q) },
startpage: { name: "Startpage", url: (q) => "https://www.startpage.com/sp/search?query=" + encodeURIComponent(q) },
};
const SEARCH = (q) => (SEARCH_ENGINES[settings.searchEngine] || SEARCH_ENGINES.duckduckgo).url(q);
// Public content relay (secret-free): serves s3/ip/h/u without shipping keys.
const GATEWAY = "https://navigate.st";
@ -71,6 +79,7 @@ const SETTINGS_DEFAULTS = {
timezoneValue: "UTC", // IANA zone used when timezoneMode === "change"
languageMode: "show", // show (real) | hide (en-US) | change (languageValue)
languageValue: "en-US", // locale used when languageMode === "change"
searchEngine: "duckduckgo",// default search engine (see SEARCH_ENGINES)
};
let settings = { ...SETTINGS_DEFAULTS };
const settingsFile = () => path.join(app.getPath("userData"), "settings.json");
@ -81,6 +90,12 @@ function loadSettings() {
function saveSettings() {
try { fs.writeFileSync(settingsFile(), JSON.stringify(settings, null, 2)); } catch (e) { console.error("settings save failed:", e.message); }
}
// ---- bookmarks / saved pages (userData/bookmarks.json) ----
let bookmarks = [];
const bookmarksFile = () => path.join(app.getPath("userData"), "bookmarks.json");
function loadBookmarks() { try { if (fs.existsSync(bookmarksFile())) bookmarks = JSON.parse(fs.readFileSync(bookmarksFile(), "utf8")); } catch (e) { console.error("bookmarks load failed:", e.message); } }
function saveBookmarks() { try { fs.writeFileSync(bookmarksFile(), JSON.stringify(bookmarks, null, 2)); } catch (e) { console.error("bookmarks save failed:", e.message); } }
function emitBookmarks() { try { chrome?.webContents.send("bookmarks", bookmarks); } catch {} }
// WebRTC IP-handling policy: protect by default (independent of Tor), strongest under Tor.
function webrtcPolicy() {
if (!settings.webrtcProtect) return "default";
@ -340,8 +355,12 @@ async function serveBns(request) {
// ---- window + tabs ----
let win, chrome, statusbar;
let CHROME_H = 84; // grows when an extra bar (Tor notice / BCNR offer / site info) is shown
let CHROME_H = 84; // grows when an extra bar (Tor notice / BCNR offer) is shown
const STATUS_H = 24; // fixed bottom resolver/provenance line
// Site-info popover: a floating overlay VIEW on top of the page content, so it
// never pushes the page down. Positioned under the address-bar badge on demand.
let popover, popVisible = false, popPos = { x: 8, y: 90 };
const POP_W = 360, POP_H = 200;
const tabs = []; // { id, view, title, url, prov }
let activeId = null, tabSeq = 0;
const tabById = (id) => tabs.find((t) => t.id === id);
@ -352,6 +371,7 @@ const activeTab = () => tabById(activeId);
function pushNav(prov) {
chrome?.webContents.send("nav", prov);
statusbar?.webContents.send("nav", prov);
if (popVisible) popover?.webContents.send("site-info", prov);
}
function layout() {
@ -361,9 +381,28 @@ function layout() {
const bodyH = Math.max(0, height - CHROME_H - STATUS_H);
for (const t of tabs) t.view.setBounds({ x: 0, y: CHROME_H, width, height: bodyH });
statusbar?.setBounds({ x: 0, y: height - STATUS_H, width, height: STATUS_H });
positionPopover();
}
function positionPopover() {
if (!popover) return;
const { width } = win.getContentBounds();
const x = Math.max(6, Math.min(popPos.x, width - POP_W - 6));
popover.setBounds({ x, y: popPos.y, width: POP_W, height: POP_H });
}
function showPopover(show) {
if (!popover) return;
if (show) {
positionPopover();
// Re-add to the top of the z-order (tabs added later would otherwise cover it).
win.contentView.removeChildView(popover);
win.contentView.addChildView(popover);
popover.setVisible(true); popVisible = true;
popover.webContents.send("site-info", activeTab()?.prov || { kind: "home" });
} else { popover.setVisible(false); popVisible = false; }
}
function setActive(id) {
activeId = id;
if (popVisible) showPopover(false); // don't carry a stale popover across tabs
for (const t of tabs) t.view.setVisible(t.id === id);
const t = activeTab();
if (t?.prov) pushNav(t.prov);
@ -466,6 +505,12 @@ function createWindow() {
statusbar.webContents.loadFile("statusbar.html");
// The bottom bar loads async; once ready, paint it with the active tab's state.
statusbar.webContents.once("did-finish-load", () => { const t = activeTab(); if (t?.prov) statusbar.webContents.send("nav", t.prov); });
// Floating site-info overlay (hidden until the address-bar badge is clicked).
popover = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "popover-preload.js") } });
try { popover.setBackgroundColor("#00000000"); } catch {}
win.contentView.addChildView(popover);
popover.webContents.loadFile("popover.html");
popover.setVisible(false);
chrome.webContents.once("did-finish-load", () => {
const saved = settings.restoreSession ? loadSession() : [];
if (saved.length) saved.forEach((u) => createTab(u)); else createTab();
@ -548,6 +593,33 @@ ipcMain.handle("go-forward", () => { const wc = activeTab()?.view.webContents; i
ipcMain.handle("reload", () => activeTab()?.view.webContents.reload());
ipcMain.handle("toggle-tor", () => { torState === "off" ? startTor() : stopTor(); });
ipcMain.handle("open-settings", () => { const ex = tabs.find((t) => t.settings); if (ex) return setActive(ex.id); createTab(null, { settings: true }); });
ipcMain.handle("toggle-site-info", (_e, rect) => {
if (popVisible) return showPopover(false);
if (rect) popPos = { x: Math.round(rect.x), y: Math.round(rect.y) };
showPopover(true);
});
ipcMain.handle("close-site-info", () => showPopover(false));
ipcMain.handle("search-engines", () => ({
engines: Object.entries(SEARCH_ENGINES).map(([id, e]) => ({ id, name: e.name })),
current: settings.searchEngine,
}));
ipcMain.handle("set-search-engine", (_e, id) => {
if (SEARCH_ENGINES[id]) { settings.searchEngine = id; saveSettings(); }
return settings.searchEngine;
});
ipcMain.handle("bookmarks-get", () => bookmarks);
ipcMain.handle("bookmark-add", (_e, bm) => {
if (bm && bm.url && !bookmarks.some((b) => b.url === bm.url)) {
bookmarks.push({ title: bm.title || bm.url, url: bm.url });
saveBookmarks(); emitBookmarks();
}
return bookmarks;
});
ipcMain.handle("bookmark-remove", (_e, url) => {
bookmarks = bookmarks.filter((b) => b.url !== url);
saveBookmarks(); emitBookmarks();
return bookmarks;
});
ipcMain.handle("settings-get", () => settings);
ipcMain.handle("settings-set", (_e, key, val) => {
if (key in SETTINGS_DEFAULTS) { settings[key] = val; saveSettings(); }
@ -572,7 +644,9 @@ ipcMain.handle("switch-to-bcnr", () => {
// launching the full UI (see dev/selftest.js). Normal `npm start` is unchanged.
if (!process.env.THESEUS_NO_AUTOSTART) {
app.whenReady().then(() => {
Menu.setApplicationMenu(null); // drop the native File/Edit/View/Help menu bar
loadSettings();
loadBookmarks();
applyPermissions();
applyAcceptLanguage();
protocol.handle("bns", serveBns);

5
popover-preload.js Normal file
View file

@ -0,0 +1,5 @@
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("pop", {
onData: (cb) => ipcRenderer.on("site-info", (_e, d) => cb(d)),
close: () => ipcRenderer.invoke("close-site-info"),
});

47
popover.html Normal file
View file

@ -0,0 +1,47 @@
<!doctype html>
<html><head><meta charset="utf-8">
<style>
:root { color-scheme: dark; font-family: system-ui, sans-serif; }
html, body { margin: 0; height: 100%; background: transparent; }
.card { height: 100%; display: flex; flex-direction: column; background: #141b28;
border: 1px solid #ffffff24; border-radius: 10px; box-shadow: 0 12px 34px #000b; overflow: hidden; }
.head { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px;
border-bottom: 1px solid #ffffff12; font-size: 12px; color: #b9c2d0; }
.head .x { border: none; background: transparent; color: #8b98a9; cursor: pointer; font-size: 13px; border-radius: 5px; padding: 1px 6px; }
.head .x:hover { background: #ffffff14; color: #fff; }
.body { display: grid; grid-template-columns: auto 1fr; gap: 6px 14px; padding: 11px 14px; font-size: 12px; overflow-y: auto; }
.k { color: #7f8aa0; } .v { color: #e7eaf1; }
.v code { font-family: ui-monospace, monospace; color: #bfeae4; word-break: break-all; }
.dotv { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #5e6678; margin-right: 6px; }
.dotv.chain { background: #d6ff3d; } .dotv.sia { background: #b39ddb; }
.dotv.server { background: #4fd1a5; } .dotv.web { background: #8b93a7; } .dotv.err { background: #f6768a; }
</style></head>
<body>
<div class="card">
<div class="head"><span>Site information</span><button class="x" id="x" title="Close"></button></div>
<div class="body" id="body"></div>
</div>
<script>
const $ = (id) => document.getElementById(id);
$("x").onclick = () => window.pop.close();
const esc = (s) => String(s || "").replace(/</g, "&lt;");
const row = (k, v) => `<span class="k">${k}</span><span class="v">${v}</span>`;
window.pop.onData((d) => {
let html = "";
if (!d || d.kind === "home") html = row("Page", "Theseus home");
else if (d.kind === "web") html = row("Site", `<span class="dotv web"></span><code>${esc(d.host)}</code>`) + row("Resolved via", "ICANN DNS (web)") + row("Registry", "ICANN");
else if (d.kind === "resolving") html = row("Site", `<code>${esc(d.host)}</code>`) + row("Status", "resolving on the BCH chain…");
else if (d.kind === "nxdomain") html = row("Name", `<span class="dotv err"></span><code>${esc(d.host)}</code>`) + row("Status", "not registered (NXDOMAIN)");
else if (d.kind === "error") html = row("Status", `<span class="dotv err"></span>${esc(d.error || "resolution failed")}`);
else {
const cls = (d.source || "").includes("chain") ? "chain" : (d.source || "").includes("Sia") ? "sia" : (d.source || "").includes("server") ? "server" : "web";
html = row("Site", `<span class="dotv ${cls}"></span><code>${esc(d.host)}</code>`) +
row("Served from", esc(d.source) || "chain") +
row("Registry", (d.registry || "BCNR") + (d.tld ? " · ." + d.tld : "")) +
(d.records && d.records.length ? row("Records", `<code>${esc(d.records.join(", "))}</code>`) : "") +
(d.category ? row("Certificate", `<code>${esc(String(d.category).slice(0, 24))}…</code>`) : "");
}
$("body").innerHTML = html;
});
</script>
</body></html>

View file

@ -11,8 +11,15 @@ contextBridge.exposeInMainWorld("theseus", {
reload: () => ipcRenderer.invoke("reload"),
toggleTor: () => ipcRenderer.invoke("toggle-tor"),
openSettings: () => ipcRenderer.invoke("open-settings"),
toggleSiteInfo: (rect) => ipcRenderer.invoke("toggle-site-info", rect),
switchToBcnr: () => ipcRenderer.invoke("switch-to-bcnr"),
setChromeHeight: (h) => ipcRenderer.invoke("set-chrome-height", h),
getSearchEngines: () => ipcRenderer.invoke("search-engines"),
setSearchEngine: (id) => ipcRenderer.invoke("set-search-engine", id),
getBookmarks: () => ipcRenderer.invoke("bookmarks-get"),
addBookmark: (bm) => ipcRenderer.invoke("bookmark-add", bm),
removeBookmark: (url) => ipcRenderer.invoke("bookmark-remove", url),
onBookmarks: (cb) => ipcRenderer.on("bookmarks", (_e, d) => cb(d)),
onNav: (cb) => ipcRenderer.on("nav", (_e, d) => cb(d)),
onTor: (cb) => ipcRenderer.on("tor", (_e, d) => cb(d)),
onTabs: (cb) => ipcRenderer.on("tabs", (_e, d) => cb(d)),