diff --git a/changelog.d/features/12306-headroom-url.md b/changelog.d/features/12306-headroom-url.md new file mode 100644 index 000000000000..7478e094f895 --- /dev/null +++ b/changelog.d/features/12306-headroom-url.md @@ -0,0 +1 @@ +- **feat(settings):** persist `headroomUrl` through Settings so status/start use the operator URL instead of only `HEADROOM_URL` ([#12306](https://github.com/diegosouzapw/OmniRoute/issues/12306)) diff --git a/src/app/(dashboard)/dashboard/settings/advanced/page.tsx b/src/app/(dashboard)/dashboard/settings/advanced/page.tsx index 5dfefd170be1..af8eb23deb83 100644 --- a/src/app/(dashboard)/dashboard/settings/advanced/page.tsx +++ b/src/app/(dashboard)/dashboard/settings/advanced/page.tsx @@ -5,6 +5,7 @@ import LogToolSourcesCard from "../components/LogToolSourcesCard"; import PayloadRulesTab from "../components/PayloadRulesTab"; import RequestLimitsTab from "../components/RequestLimitsTab"; import CliproxyapiSettingsTab from "../components/CliproxyapiSettingsTab"; +import HeadroomProxyCard from "../components/HeadroomProxyCard"; export default function SettingsAdvancedPage() { return ( @@ -14,6 +15,7 @@ export default function SettingsAdvancedPage() { + ); } diff --git a/src/app/(dashboard)/dashboard/settings/components/HeadroomProxyCard.tsx b/src/app/(dashboard)/dashboard/settings/components/HeadroomProxyCard.tsx new file mode 100644 index 000000000000..de37387cec57 --- /dev/null +++ b/src/app/(dashboard)/dashboard/settings/components/HeadroomProxyCard.tsx @@ -0,0 +1,235 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; +import { useTranslations } from "next-intl"; +import { Card, Button, Input } from "@/shared/components"; +import { isHttpUrl } from "@/shared/validation/schemas/misc"; + +const HEADROOM_URL_MAX = 500; + +function isValidHeadroomUrl(value: string): boolean { + const trimmed = value.trim(); + if (trimmed === "") return true; + return trimmed.length <= HEADROOM_URL_MAX && isHttpUrl(trimmed); +} + +type SettingsErrorBody = { + error?: { + message?: string; + details?: { field?: string; message?: string }[]; + }; +}; + +function settingsErrorText(body: SettingsErrorBody, fallback: string): string { + const first = body.error?.details?.[0]; + if (first?.message) { + return first.field ? `${first.field}: ${first.message}` : first.message; + } + return body.error?.message || fallback; +} + +interface HeadroomStatus { + url?: string; + running?: boolean; + canStart?: boolean; + localUrl?: boolean; + installed?: boolean; +} + +export default function HeadroomProxyCard() { + const t = useTranslations("settings"); + const [url, setUrl] = useState(""); + const [loaded, setLoaded] = useState(false); + const [saving, setSaving] = useState(false); + const [acting, setActing] = useState(false); + const [status, setStatus] = useState(null); + const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null); + const saveAc = useRef(null); + const lifecycleAc = useRef(null); + + const refreshStatus = useCallback(async (signal?: AbortSignal) => { + const res = await fetch("/api/headroom/status", signal ? { signal } : undefined); + if (!res.ok) return; + const data = (await res.json()) as HeadroomStatus; + if (signal?.aborted) return; + setStatus(data); + }, []); + + useEffect(() => { + const ac = new AbortController(); + // Async continuation so every setState happens after an await + // (react-hooks/set-state-in-effect: no synchronous setState in effect bodies). + void (async () => { + try { + const r = await fetch("/api/settings", { signal: ac.signal }); + const data = (r.ok ? await r.json() : {}) as Record; + if (ac.signal.aborted) return; + if (typeof data.headroomUrl === "string") setUrl(data.headroomUrl); + } catch { + // ignore + } finally { + if (!ac.signal.aborted) setLoaded(true); + } + // Status is for start/stop buttons only. Do not copy status.url into the + // input -- that value is HEADROOM_URL fallback and would overwrite empty. + try { + await refreshStatus(ac.signal); + } catch { + // ignore + } + })(); + return () => { + ac.abort(); + saveAc.current?.abort(); + lifecycleAc.current?.abort(); + }; + }, [refreshStatus]); + + const save = useCallback(async () => { + if (!isValidHeadroomUrl(url)) { + setMsg({ ok: false, text: t("cliproxyapiInvalidUrl") }); + return; + } + saveAc.current?.abort(); + const ac = new AbortController(); + saveAc.current = ac; + const { signal } = ac; + setSaving(true); + setMsg(null); + const trimmed = url.trim(); + try { + const res = await fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ headroomUrl: trimmed }), + signal, + }); + if (!res.ok) { + const body = (await res.json().catch(() => ({}))) as SettingsErrorBody; + throw new Error(settingsErrorText(body, `HTTP ${res.status}`)); + } + if (signal.aborted) return; + setUrl(trimmed); + setMsg({ ok: true, text: t("settingSaved") }); + } catch (error) { + if (signal.aborted) return; + setMsg({ + ok: false, + text: error instanceof Error ? error.message : t("settingSaveFailed"), + }); + return; + } finally { + if (saveAc.current === ac) setSaving(false); + } + try { + await refreshStatus(signal); + } catch { + // PATCH already succeeded; status is best-effort. + } + }, [url, t, refreshStatus]); + + const postLifecycle = useCallback( + async (path: "/api/headroom/start" | "/api/headroom/stop") => { + lifecycleAc.current?.abort(); + const ac = new AbortController(); + lifecycleAc.current = ac; + const { signal } = ac; + setActing(true); + setMsg(null); + try { + const res = await fetch(path, { method: "POST", signal }); + if (!res.ok) { + const body = (await res.json().catch(() => ({}))) as SettingsErrorBody; + throw new Error(settingsErrorText(body, `HTTP ${res.status}`)); + } + try { + await refreshStatus(signal); + } catch { + // start/stop already succeeded; status is best-effort. + } + } catch (error) { + if (signal.aborted) return; + setMsg({ + ok: false, + text: error instanceof Error ? error.message : t("settingSaveFailed"), + }); + } finally { + if (lifecycleAc.current === ac) setActing(false); + } + }, + [refreshStatus, t] + ); + + if (!loaded) return null; + + const canStart = status?.canStart === true; + const running = status?.running === true; + const busy = saving || acting; + + return ( + + + + compress + + + {t("headroomProxyTitle")} + {t("headroomProxyDesc")} + + + + {msg && ( + + + {msg.ok ? "check_circle" : "error"} + + {msg.text} + + )} + + + + {t("headroomProxyUrl")} + setUrl(e.target.value)} + placeholder="http://localhost:8787" + className="w-full" + disabled={busy} + /> + {t("headroomProxyUrlHint")} + + + void save()} disabled={busy}> + {t("headroomProxySave")} + + void postLifecycle("/api/headroom/start")} + disabled={busy || !canStart} + > + {t("headroomProxyStart")} + + void postLifecycle("/api/headroom/stop")} + disabled={busy || !running} + > + {t("headroomProxyStop")} + + + {status && !canStart && !status.localUrl && ( + {t("headroomProxyExternalHint")} + )} + + + ); +} diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index d33a7633bf7a..00fd85e21c27 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "الصحة", "cliproxyapiPort": "منفذ", "qdrantHost": "مضيف", - "qdrantCollection": "مجموعة" + "qdrantCollection": "مجموعة", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "محرك آر تي كيه", diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json index a50b88762fce..7d3eed8c7ecf 100644 --- a/src/i18n/messages/az.json +++ b/src/i18n/messages/az.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Sağlamlıq", "cliproxyapiPort": "Port", "qdrantHost": "Ev sahibi", - "qdrantCollection": "Kolleksiya" + "qdrantCollection": "Kolleksiya", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index 8086f96fc51e..b83456a25341 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Здраве", "cliproxyapiPort": "Порт", "qdrantHost": "Хост", - "qdrantCollection": "Колекция" + "qdrantCollection": "Колекция", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index 507007dc7bec..767b25ffd615 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "স্বাস্থ্য", "cliproxyapiPort": "পোর্ট", "qdrantHost": "হোস্ট", - "qdrantCollection": "সংগ্রহ" + "qdrantCollection": "সংগ্রহ", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index f68aa4b51d64..d23ce1eb6137 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Zdraví", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Kolekce" + "qdrantCollection": "Kolekce", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index f326e8f31330..5c26437fb605 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Sundhed", "cliproxyapiPort": "Port", "qdrantHost": "Vært", - "qdrantCollection": "Samling" + "qdrantCollection": "Samling", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 4b5153621382..6ded55b40a94 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Gesundheit", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Sammlung" + "qdrantCollection": "Sammlung", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 292d76c86d56..dd1c6a11f652 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -7907,6 +7907,14 @@ "cliproxyapiFallback": "CLIProxyAPI Fallback", "cliproxyapiEnableFallback": "Enable CLIProxyAPI Fallback", "cliproxyapiUrl": "CLIProxyAPI URL", + "headroomProxyTitle": "Headroom proxy", + "headroomProxyDesc": "URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "Headroom URL", + "headroomProxyUrlHint": "Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "Save", + "headroomProxyStart": "Start", + "headroomProxyStop": "Stop", + "headroomProxyExternalHint": "This URL is not loopback, so OmniRoute will not spawn the local CLI.", "cliproxyapiStatus": "CLIProxyAPI Status", "cliproxyapiNotDetected": "Not detected", "cliproxyapiImportAuthTitle": "Import accounts from CLIProxyAPI", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index b1fbcc4fe6aa..c9ca5f5f3e96 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Salud", "cliproxyapiPort": "Puerto", "qdrantHost": "Anfitrión", - "qdrantCollection": "Colección" + "qdrantCollection": "Colección", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index b57be4497626..3186d8a26a3e 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "سلامت", "cliproxyapiPort": "پورت", "qdrantHost": "میزبان", - "qdrantCollection": "مجموعه" + "qdrantCollection": "مجموعه", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index 0b220d98b7dc..3b40f11caf5e 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Terveys", "cliproxyapiPort": "Portti", "qdrantHost": "Isäntä", - "qdrantCollection": "Kokoelma" + "qdrantCollection": "Kokoelma", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 59cae5566030..bee6010d70eb 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Santé", "cliproxyapiPort": "Port", "qdrantHost": "Hôte", - "qdrantCollection": "Collection" + "qdrantCollection": "Collection", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index 152237a60684..4becbd4de1a0 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "આરોગ્ય", "cliproxyapiPort": "પોર્ટ", "qdrantHost": "હોસ્ટ", - "qdrantCollection": "સંગ્રહ" + "qdrantCollection": "સંગ્રહ", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index 7f45b461c0e3..ca44b00147fb 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "בריאות", "cliproxyapiPort": "פורט", "qdrantHost": "מארח", - "qdrantCollection": "אוסף" + "qdrantCollection": "אוסף", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 3867eeb43a92..9730073a6229 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "स्वास्थ्य", "cliproxyapiPort": "पोर्ट", "qdrantHost": "होस्ट", - "qdrantCollection": "संग्रह" + "qdrantCollection": "संग्रह", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index 53344828373d..d033965d4fbc 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Egészség", "cliproxyapiPort": "Port", "qdrantHost": "Gazda", - "qdrantCollection": "Gyűjtemény" + "qdrantCollection": "Gyűjtemény", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index 545e9a5a413c..12ce90c987c3 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Kesehatan", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Koleksi" + "qdrantCollection": "Koleksi", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index dc01def70821..e5a576176043 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Salute", "cliproxyapiPort": "Porta", "qdrantHost": "Host", - "qdrantCollection": "Collezione" + "qdrantCollection": "Collezione", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index ba8c06446240..a4162c3b157f 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "健康", "cliproxyapiPort": "ポート", "qdrantHost": "ホスト", - "qdrantCollection": "コレクション" + "qdrantCollection": "コレクション", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index ab4b4a67f7a8..64275df52294 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "건강", "cliproxyapiPort": "포트", "qdrantHost": "호스트", - "qdrantCollection": "컬렉션" + "qdrantCollection": "컬렉션", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index 53af82d57aa2..d340f6391756 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "आरोग्य", "cliproxyapiPort": "पोर्ट", "qdrantHost": "होस्ट", - "qdrantCollection": "संग्रह" + "qdrantCollection": "संग्रह", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index 05b504ffded7..f6b839fd7ee7 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Kesihatan", "cliproxyapiPort": "Pelabuhan", "qdrantHost": "Hos", - "qdrantCollection": "Koleksi" + "qdrantCollection": "Koleksi", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index c50a7beebc9a..db912312d2d6 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Gezondheid", "cliproxyapiPort": "Haven", "qdrantHost": "Host", - "qdrantCollection": "Verzameling" + "qdrantCollection": "Verzameling", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index 5f7a1c64b086..4394a1e27b23 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Helse", "cliproxyapiPort": "Port", "qdrantHost": "Vert", - "qdrantCollection": "Samling" + "qdrantCollection": "Samling", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index 3a3080eded40..89183a593d33 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Kalusugan", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Koleksyon" + "qdrantCollection": "Koleksyon", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index bd07b8321e30..8f5c91da844d 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Zdrowie", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Kolekcja" + "qdrantCollection": "Kolekcja", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "Silnik RTK", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 7c4d1eb12c40..af375ee87ad1 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -8341,7 +8341,15 @@ "cliproxyapiHealth": "Saúde", "cliproxyapiPort": "Porta", "qdrantHost": "Host", - "qdrantCollection": "Coleção" + "qdrantCollection": "Coleção", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "Motor RTK", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 3533f6370ad2..6cd1a1aa6c93 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Saúde", "cliproxyapiPort": "Porto", "qdrantHost": "Anfitrião", - "qdrantCollection": "Coleção" + "qdrantCollection": "Coleção", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "Motor RTK", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index afe466f408f1..4f445c4476c9 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Sănătate", "cliproxyapiPort": "Port", "qdrantHost": "Gazdă", - "qdrantCollection": "Colecție" + "qdrantCollection": "Colecție", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index dbd845f38352..d740cc5fafb5 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Здоровье", "cliproxyapiPort": "Порт", "qdrantHost": "Хост", - "qdrantCollection": "Коллекция" + "qdrantCollection": "Коллекция", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index 40f2003b7771..5dcf60ac13b2 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Zdravie", "cliproxyapiPort": "Port", "qdrantHost": "Host", - "qdrantCollection": "Zbierka" + "qdrantCollection": "Zbierka", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index 175c8e1f97a6..bc502df18d64 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Hälsa", "cliproxyapiPort": "Port", "qdrantHost": "Värd", - "qdrantCollection": "Samling" + "qdrantCollection": "Samling", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index b8288d386330..7301fb6774d4 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Afya", "cliproxyapiPort": "Bandari", "qdrantHost": "Mwenyeji", - "qdrantCollection": "Mkusanyiko" + "qdrantCollection": "Mkusanyiko", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index e0614361fc76..346590dc7ffc 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "ஆரோக்கியம்", "cliproxyapiPort": "போர்ட்", "qdrantHost": "விருந்தினர்", - "qdrantCollection": "கலெக்ஷன்" + "qdrantCollection": "கலெக்ஷன்", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index 6dfa5290b7c4..5cbdf5c8852f 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "ఆరోగ్యం", "cliproxyapiPort": "పోర్ట్", "qdrantHost": "హోస్ట్", - "qdrantCollection": "సేకరణ" + "qdrantCollection": "సేకరణ", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index 126df99e1b41..68f90b88a60a 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "สุขภาพ", "cliproxyapiPort": "พอร์ต", "qdrantHost": "โฮสต์", - "qdrantCollection": "การรวบรวม" + "qdrantCollection": "การรวบรวม", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index 5ee309e90d1b..6ed5f6893337 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Sağlık", "cliproxyapiPort": "Port", "qdrantHost": "Ana Bilgisayar", - "qdrantCollection": "Koleksiyon" + "qdrantCollection": "Koleksiyon", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index e1203b1d1bb8..5601b38668f6 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "Здоров'я", "cliproxyapiPort": "Порт", "qdrantHost": "Хост", - "qdrantCollection": "Колекція" + "qdrantCollection": "Колекція", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "Двигун RTK", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index fdc539197616..e22f74209058 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "صحت", "cliproxyapiPort": "پورٹ", "qdrantHost": "میزبان", - "qdrantCollection": "اجتماع" + "qdrantCollection": "اجتماع", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index 61287b04463b..bfbd5261d50a 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -8341,7 +8341,15 @@ "cliproxyapiHealth": "Sức Khỏe", "cliproxyapiPort": "Cổng", "qdrantHost": "Máy chủ", - "qdrantCollection": "Bộ Sưu Tập" + "qdrantCollection": "Bộ Sưu Tập", + "headroomProxyTitle": "Proxy Headroom", + "headroomProxyDesc": "URL của proxy tiết kiệm token Headroom (tùy chọn). Để trống thì dùng HEADROOM_URL hoặc http://localhost:8787.", + "headroomProxyUrl": "URL Headroom", + "headroomProxyUrlHint": "URL loopback có thể khởi chạy từ trang này. URL bên ngoài chỉ được kiểm tra.", + "headroomProxySave": "Lưu", + "headroomProxyStart": "Bắt đầu", + "headroomProxyStop": "Dừng", + "headroomProxyExternalHint": "URL này không phải loopback, nên OmniRoute sẽ không khởi chạy CLI cục bộ." }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 4306b98ac2fc..8501b6d0b4e4 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "健康", "cliproxyapiPort": "端口", "qdrantHost": "主机", - "qdrantCollection": "集合" + "qdrantCollection": "集合", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "命令输出过滤引擎", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 1a4db6102bff..66689e90571a 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -8340,7 +8340,15 @@ "cliproxyapiHealth": "健康", "cliproxyapiPort": "埠", "qdrantHost": "主機", - "qdrantCollection": "集合" + "qdrantCollection": "集合", + "headroomProxyTitle": "__MISSING__:Headroom proxy", + "headroomProxyDesc": "__MISSING__:URL of the optional Headroom token-saver proxy. Empty uses HEADROOM_URL or http://localhost:8787.", + "headroomProxyUrl": "__MISSING__:Headroom URL", + "headroomProxyUrlHint": "__MISSING__:Loopback URLs can be started from this page. External URLs are probed only.", + "headroomProxySave": "__MISSING__:Save", + "headroomProxyStart": "__MISSING__:Start", + "headroomProxyStop": "__MISSING__:Stop", + "headroomProxyExternalHint": "__MISSING__:This URL is not loopback, so OmniRoute will not spawn the local CLI." }, "contextRtk": { "title": "RTK 引擎", diff --git a/src/shared/validation/settingsSchemas.ts b/src/shared/validation/settingsSchemas.ts index 55f1c4964502..a07651d587a4 100644 --- a/src/shared/validation/settingsSchemas.ts +++ b/src/shared/validation/settingsSchemas.ts @@ -23,6 +23,7 @@ import { SPAWN_CAPABLE_PREFIXES, SPAWN_CAPABLE_PATTERN_ANCESTORS, } from "@/shared/constants/spawnCapablePrefixes"; +import { isHttpUrl } from "@/shared/validation/schemas/misc"; const signatureCacheModeValues = ["enabled", "bypass", "bypass-strict"] as const; @@ -493,6 +494,26 @@ export const updateSettingsSchema = z.object({ // CLIProxyAPI connection settings cliproxyapi_fallback_enabled: z.boolean().optional(), cliproxyapi_url: z.string().url().max(500).optional(), + // #12306: external Headroom proxy URL. Empty = fall back to HEADROOM_URL / localhost:8787. + // Status/start already read this key; without the schema field PATCH strips it. + // Trim first so a padded URL matches the client (isValidHeadroomUrl trims) + // and whitespace-only becomes the empty fallback, not "Invalid URL". + // z.string().url() also accepts javascript:/data:/file:. probeProxyRunning + // interpolates this into fetch(`${url}/health`), so restrict to http(s). + headroomUrl: z + .string() + .trim() + .pipe( + z.union([ + z.literal(""), + z + .string() + .url() + .max(500) + .refine((value) => isHttpUrl(value), "must be an http(s) URL"), + ]) + ) + .optional(), cliproxyapi_fallback_codes: z.string().max(200).optional(), // #7645: dedicated CLIProxyAPI credential. CLIProxyAPI requires its own // separately-configured `api-keys:` credential and rejects any other token diff --git a/tests/unit/headroom-url-settings-12306.test.ts b/tests/unit/headroom-url-settings-12306.test.ts new file mode 100644 index 000000000000..e9c89c8638f8 --- /dev/null +++ b/tests/unit/headroom-url-settings-12306.test.ts @@ -0,0 +1,153 @@ +/** + * #12306: settings.headroomUrl must survive PATCH /api/settings. + * + * Status/start already READ settings.headroomUrl. Without the schema + * field Zod strips the key and the write path is a no-op. + */ +import { test, after } from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const testDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-12306-headroom-")); +const originalDataDir = process.env.DATA_DIR; +process.env.DATA_DIR = testDataDir; + +const { updateSettingsSchema } = await import("../../src/shared/validation/settingsSchemas.ts"); +const coreDb = await import("../../src/lib/db/core.ts"); +const settingsDb = await import("../../src/lib/db/settings.ts"); + +after(() => { + coreDb.resetDbInstance(); + if (fs.existsSync(testDataDir)) { + fs.rmSync(testDataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); + } + if (originalDataDir === undefined) delete process.env.DATA_DIR; + else process.env.DATA_DIR = originalDataDir; +}); + +test("updateSettingsSchema keeps a valid headroomUrl", () => { + const parsed = updateSettingsSchema.parse({ + headroomUrl: "http://127.0.0.1:8787", + }); + assert.equal(parsed.headroomUrl, "http://127.0.0.1:8787"); +}); + +test("updateSettingsSchema accepts an empty headroomUrl to fall back to HEADROOM_URL", () => { + const parsed = updateSettingsSchema.parse({ headroomUrl: "" }); + assert.equal(parsed.headroomUrl, ""); +}); + +test("updateSettingsSchema trims a padded headroomUrl before validating", () => { + const parsed = updateSettingsSchema.parse({ + headroomUrl: " http://headroom.internal:9090 ", + }); + assert.equal(parsed.headroomUrl, "http://headroom.internal:9090"); +}); + +test("updateSettingsSchema treats whitespace-only headroomUrl as empty", () => { + const parsed = updateSettingsSchema.parse({ headroomUrl: " " }); + assert.equal(parsed.headroomUrl, ""); +}); + +test("updateSettingsSchema rejects a non-URL headroomUrl", () => { + const result = updateSettingsSchema.safeParse({ headroomUrl: "not-a-url" }); + assert.equal(result.success, false); +}); + +test("updateSettingsSchema rejects non-http(s) headroomUrl schemes", () => { + for (const url of [ + "javascript:alert(1)", + "ftp://x", + "data:text/html,x", + "file:///etc/passwd", + "http://", + "http://[", + "http:", + ]) { + const result = updateSettingsSchema.safeParse({ headroomUrl: url }); + assert.equal(result.success, false, url); + } +}); + +test("updateSettingsSchema rejects a headroomUrl over 500 chars", () => { + const result = updateSettingsSchema.safeParse({ + headroomUrl: `http://example.com/${"x".repeat(500)}`, + }); + assert.equal(result.success, false); +}); + +test("updateSettings round-trips a validated headroomUrl", async () => { + await coreDb.ensureDbInitialized(); + const parsed = updateSettingsSchema.parse({ + headroomUrl: "http://headroom.internal:9090", + }); + await settingsDb.updateSettings(parsed); + const stored = await settingsDb.getSettings(); + assert.equal(stored.headroomUrl, "http://headroom.internal:9090"); +}); + +test("updateSettings round-trips an empty headroomUrl without dropping the key", async () => { + await coreDb.ensureDbInitialized(); + await settingsDb.updateSettings( + updateSettingsSchema.parse({ headroomUrl: "http://headroom.internal:9090" }) + ); + await settingsDb.updateSettings(updateSettingsSchema.parse({ headroomUrl: "" })); + const stored = await settingsDb.getSettings(); + assert.equal(stored.headroomUrl, ""); +}); + +test("advanced settings page mounts the Headroom proxy card", async () => { + const src = fs.readFileSync( + path.join(import.meta.dirname, "../../src/app/(dashboard)/dashboard/settings/advanced/page.tsx"), + "utf8" + ); + assert.match(src, /HeadroomProxyCard/); +}); + +test("after() restores DATA_DIR so later files in the same process keep their own dir", () => { + const src = fs.readFileSync(new URL(import.meta.url), "utf8"); + assert.match(src, /const originalDataDir = process\.env\.DATA_DIR/); + assert.match(src, /if \(originalDataDir === undefined\) delete process\.env\.DATA_DIR/); +}); + +test("save reads PATCH validation details instead of a generic HTTP status", () => { + const src = fs.readFileSync( + path.join( + import.meta.dirname, + "../../src/app/(dashboard)/dashboard/settings/components/HeadroomProxyCard.tsx" + ), + "utf8" + ); + assert.match(src, /throw new Error\(settingsErrorText\(body, `HTTP \$\{res\.status\}`\)\)/); + assert.equal( + (src.match(/throw new Error\(settingsErrorText\(body, `HTTP \$\{res\.status\}`\)\)/g) || []).length, + 2 + ); + assert.match(src, /body\.error\?\.details/); + assert.match(src, /isHttpUrl/); + assert.match(src, /const HEADROOM_URL_MAX = 500/); + assert.match(src, /setUrl\(trimmed\)/); + assert.match(src, /const saveAc = useRef\(null\)/); + assert.match(src, /const lifecycleAc = useRef\(null\)/); + assert.match(src, /saveAc\.current = ac/); + assert.match(src, /lifecycleAc\.current = ac/); + assert.match(src, /await fetch\(path, \{ method: "POST", signal \}\)/); + assert.match(src, /body: JSON.stringify\(\{ headroomUrl: trimmed \}\),\s*signal,/s); + assert.match(src, /\/\/ start\/stop already succeeded; status is best-effort\./); + // Busy flags: clear only if this invocation still owns the controller. + // A second click replaces the ref; the first finally must not unlock. + assert.match(src, /if \(saveAc\.current === ac\) setSaving\(false\)/); + assert.match(src, /if \(lifecycleAc\.current === ac\) setActing\(false\)/); + assert.doesNotMatch(src, /if \(!signal\.aborted\) setSaving\(false\)/); + assert.doesNotMatch(src, /if \(!signal\.aborted\) setActing\(false\)/); + assert.match( + src, + /return \(\) => \{\s*ac\.abort\(\);\s*saveAc\.current\?\.abort\(\);\s*lifecycleAc\.current\?\.abort\(\);/s + ); + assert.match(src, /const busy = saving \|\| acting;/); + assert.match(src, /disabled=\{busy\}/); + assert.match(src, /disabled=\{busy \|\| !canStart\}/); + assert.match(src, /disabled=\{busy \|\| !running\}/); +});
{t("headroomProxyDesc")}
{t("headroomProxyUrlHint")}
{t("headroomProxyExternalHint")}