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.
This commit is contained in:
Local Dev 2026-10-01 01:24:10 +02:00
parent deae55647c
commit 93828a0172
2 changed files with 23 additions and 10 deletions

18
main.js
View file

@ -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: "<reason>"`) 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/<id>.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();

View file

@ -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 <select> is text-only) */
@ -1097,10 +1100,14 @@
// built-ins, permanently for customs).
const list = document.getElementById("engineList");
const installed = d.engines.filter((e) => e.installed);
const rowFor = (e) => `<div class="eng${e.enabled ? "" : " off"}" data-id="${e.id}" data-kind="${e.kind || "search"}" data-builtin="${e.builtin ? 1 : 0}" draggable="true">` +
// A frozen engine (see SEARCH_ENGINES in main.js) keeps its row, greyed, with the reason as its
// tooltip and an "Unavailable" badge where the switch would be.
const rowFor = (e) => `<div class="eng${e.enabled ? "" : " off"}${e.frozen ? " frozen" : ""}" data-id="${e.id}" data-kind="${e.kind || "search"}" data-builtin="${e.builtin ? 1 : 0}" draggable="true"${e.frozen ? ` title="${esc(e.frozen)}"` : ""}>` +
`<span class="grip" title="Drag to reorder">⠿</span>` +
`<span class="eic">${engIcon(e)}</span><span class="enm">${esc(e.name)}</span>` +
`<label class="sw sm" title="${e.enabled ? "Turn off" : "Turn on"}"><input type="checkbox" data-id="${e.id}" ${e.enabled ? "checked" : ""}><span class="track"><span class="knob"></span></span></label>` +
(e.frozen
? `<span class="kind warn">Unavailable</span>`
: `<label class="sw sm" title="${e.enabled ? "Turn off" : "Turn on"}"><input type="checkbox" data-id="${e.id}" ${e.enabled ? "checked" : ""}><span class="track"><span class="knob"></span></span></label>`) +
`</div>`;
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) => `<div class="cat" data-id="${e.id}">` +
const catRow = (e) => `<div class="cat${e.frozen ? " frozen" : ""}" data-id="${e.id}"${e.frozen ? ` title="${esc(e.frozen)}"` : ""}>` +
`<span class="eic">${engIcon(e)}</span>` +
`<span class="enm">${esc(e.name)}</span>` +
`<span class="kind">${(e.kind || "search") === "llm" ? "AI" : "Search"}</span>` +
`<button class="add" data-add="${e.id}">+ Add</button></div>`;
(e.frozen ? `<span class="kind warn">Unavailable</span>` : `<button class="add" data-add="${e.id}">+ Add</button>`) + `</div>`;
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");