From 93828a017267d3192887b5ff455292059c64b108 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Thu, 1 Oct 2026 01:24:10 +0200 Subject: [PATCH] fix(theseus): Presearch is frozen in the engine catalog presearch.com has redirected every request, searches included, into a dead host since 2026-09-28, so a user who picked it gets Cloudflare's origin error instead of results. Rather than deleting the entry, a catalog engine can now carry a frozen reason: it stays listed in Settings, greyed, with the reason as its tooltip and an Unavailable badge where the switch was; it is never enabled, never in the picker, never accepted as the default from any path, and a profile that had it as default falls back at startup. Turning it off still works, and deleting the field brings the engine back exactly as the user had it. --- main.js | 18 ++++++++++++------ settings.html | 15 +++++++++++---- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/main.js b/main.js index 20836a77..05429002 100644 --- a/main.js +++ b/main.js @@ -152,7 +152,7 @@ const SEARCH_ENGINES = { marginalia: { kind: "search", tier: "extra", name: "Marginalia", sym: "๐Ÿ•ธ", fav: "search.marginalia.nu", url: (q) => "https://search.marginalia.nu/search?query=" + encodeURIComponent(q) }, stract: { kind: "search", tier: "extra", name: "Stract", sym: "๐Ÿงต", fav: "stract.com", url: (q) => "https://stract.com/search?q=" + encodeURIComponent(q) }, yep: { kind: "search", tier: "extra", name: "Yep", sym: "โœณ๏ธ", fav: "yep.com", url: (q) => "https://yep.com/web?q=" + encodeURIComponent(q) }, - presearch: { kind: "search", tier: "extra", name: "Presearch", sym: "๐Ÿ”ท", fav: "presearch.com", url: (q) => "https://presearch.com/search?q=" + encodeURIComponent(q) }, + presearch: { kind: "search", tier: "extra", name: "Presearch", sym: "๐Ÿ”ท", fav: "presearch.com", frozen: "Presearch has sent every search into a dead host since 2026-09-28.", url: (q) => "https://presearch.com/search?q=" + encodeURIComponent(q) }, metager: { kind: "search", tier: "extra", name: "MetaGer", sym: "๐Ÿ‡ฉ๐Ÿ‡ช", fav: "metager.org", url: (q) => "https://metager.org/meta/meta.ger3?eingabe=" + encodeURIComponent(q) }, qwant: { kind: "search", tier: "extra", name: "Qwant", sym: "๐Ÿ‡ซ๐Ÿ‡ท", fav: "www.qwant.com", url: (q) => "https://www.qwant.com/?q=" + encodeURIComponent(q) }, swisscows: { kind: "search", tier: "extra", name: "Swisscows", sym: "๐Ÿ„", fav: "swisscows.com", url: (q) => "https://swisscows.com/en/web?query=" + encodeURIComponent(q) }, @@ -162,6 +162,10 @@ const SEARCH_ENGINES = { // Engines enabled by default (shown in the toolbar dropdown). The rest are in the // catalog and can be turned on from Settings. Custom + detected engines are always on. const DEFAULT_ENABLED = ["startpage", "duckduckgo", "google", "brave", "bing"]; +// A frozen catalog engine (`frozen: ""`) stays visible in Settings so the user sees why it +// is gone, but it is never enabled, never offered in the picker and never the default. Unfreeze by +// deleting the field; the user's installed list is left alone, so the row comes back as it was. +const isFrozen = (id) => !!(SEARCH_ENGINES[id] && SEARCH_ENGINES[id].frozen); // Search-engine icons. The catalog's icons ship inside the build // (engine-icons/.png) so the toolbar, the picker and Settings look the // same offline as online, and opening the picker no longer tells a favicon @@ -253,7 +257,7 @@ function isInstalled(id) { } function allEngines() { const list = Object.entries(SEARCH_ENGINES).map(([id, e]) => - ({ id, name: e.name, sym: e.sym, favicon: bundledIcon(id), kind: e.kind || "search", tier: e.tier || "catalog", builtin: true, installed: isInstalled(id), enabled: isEnabled(id) })); + ({ id, name: e.name, sym: e.sym, favicon: bundledIcon(id), kind: e.kind || "search", tier: e.tier || "catalog", builtin: true, installed: isInstalled(id), enabled: isEnabled(id) && !e.frozen, frozen: e.frozen || null })); for (const c of settings.customEngines || []) list.push({ id: c.id, name: c.name, sym: c.sym || "๐Ÿ”", favicon: customFavicon(c.url), kind: c.kind || "search", tier: "custom", builtin: false, installed: true, enabled: isEnabled(c.id) }); // Apply the user's custom order; ids not in engineOrder keep their natural order (stable sort). @@ -542,8 +546,8 @@ function loadSettings() { const enabled = Array.isArray(settings.enabledEngines) ? settings.enabledEngines : DEFAULT_ENABLED.slice(); const installed = Array.isArray(settings.installedEngines) ? settings.installedEngines : DEFAULT_ENABLED.slice(); settings.installedEngines = [...new Set([...installed, ...enabled])]; - if (!enabled.includes(settings.searchEngine)) { - settings.searchEngine = enabled[0] || DEFAULT_ENABLED[0]; + if (!enabled.includes(settings.searchEngine) || isFrozen(settings.searchEngine)) { + settings.searchEngine = enabled.find((x) => !isFrozen(x)) || DEFAULT_ENABLED[0]; } } catch (e) { console.warn("engine normalize:", e?.message); } } @@ -5914,7 +5918,7 @@ ipcMain.handle("clear-browsing-data", async (_e, opts) => { }); ipcMain.handle("search-engines", () => ({ engines: allEngines(), current: settings.searchEngine })); ipcMain.handle("set-search-engine", (_e, id) => { - if (allEngines().some((e) => e.id === id)) { settings.searchEngine = id; saveSettings(); emitEngines(); } + if (allEngines().some((e) => e.id === id && !e.frozen)) { settings.searchEngine = id; saveSettings(); emitEngines(); } return settings.searchEngine; }); ipcMain.handle("add-engine", async (_e, eng) => { @@ -5968,7 +5972,7 @@ ipcMain.handle("remove-from-list", (_e, id) => { // (adds to installedEngines). Disabling only removes it from enabledEngines โ€” // it stays in installedEngines so the row remains visible with the toggle off. ipcMain.handle("set-engine-enabled", (_e, id, on) => { - if (SEARCH_ENGINES[id]) { + if (SEARCH_ENGINES[id] && !(on && isFrozen(id))) { // a frozen engine can be turned off, never on let installed = (settings.installedEngines || DEFAULT_ENABLED).slice(); let enabled = (settings.enabledEngines || DEFAULT_ENABLED).filter((x) => x !== id); if (on) { @@ -6174,6 +6178,8 @@ ipcMain.handle("bookmark-move", (_e, fromUrl, targetUrl, place) => { }); ipcMain.handle("settings-get", () => settings); ipcMain.handle("settings-set", (_e, key, val) => { + // The default engine must be one that is enabled and not frozen, whichever path sets it. + if (key === "searchEngine" && !enabledEnginesList().some((e) => e.id === val)) return settings; if (key in SETTINGS_DEFAULTS) { settings[key] = val; saveSettings(); } if (key === "webrtcMode") applyWebRTCPolicy(); if (key === "dohMode" || key === "dohProvider" || key === "dohCustom") applyDoh(); diff --git a/settings.html b/settings.html index a243df31..91dfc4c6 100644 --- a/settings.html +++ b/settings.html @@ -131,6 +131,9 @@ .eng.over{border-color:var(--acid);box-shadow:0 -2px 0 var(--acid) inset} .eng.off{opacity:.55} .eng.off .enm{color:var(--mut)} + .eng.frozen,.engcat .cat.frozen{opacity:.6} + .eng .kind,.engcat .cat .kind{font-size:10.5px;letter-spacing:.05em;color:var(--dim);padding:1px 6px;border:1px solid var(--line);border-radius:999px;white-space:nowrap} + .kind.warn{color:#f6768a;border-color:rgba(246,118,138,.45)} .ehdr{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);margin:14px 0 6px;padding-top:2px} .ehdr:first-child{margin-top:0} /* default-engine dropdown: drawn by us so it can show the engine icons (a native ` + + (e.frozen + ? `Unavailable` + : ``) + ``; list.innerHTML = ENGINE_KINDS.map(({ key, label }) => { const rows = installed.filter((e) => (e.kind || "search") === key).map(rowFor).join(""); @@ -1114,11 +1121,11 @@ const cat = document.getElementById("catalogList"); const extra = document.getElementById("extraList"); const filterInput = document.getElementById("engineFilter"); - const catRow = (e) => `
` + + const catRow = (e) => `
` + `${engIcon(e)}` + `${esc(e.name)}` + `${(e.kind || "search") === "llm" ? "AI" : "Search"}` + - `
`; + (e.frozen ? `Unavailable` : ``) + `
`; const uninstalled = d.engines.filter((e) => e.builtin && !e.installed); const catalogOff = uninstalled.filter((e) => (e.tier || "catalog") === "catalog"); const extraOff = uninstalled.filter((e) => e.tier === "extra");