"use client"; import { useState, useEffect, useRef } from "react"; import { Card, Button, Toggle, Input } from "@/shared/components"; import Modal, { ConfirmModal } from "@/shared/components/Modal"; import LanguageSwitcher from "@/shared/components/LanguageSwitcher"; import { useTheme } from "@/shared/hooks/useTheme"; import { cn } from "@/shared/utils/cn"; import { APP_CONFIG } from "@/shared/constants/config"; import { LOCALE_COOKIE, normalizeLocale } from "@/i18n/config"; import { LOCALE_FLAGS } from "@/shared/constants/locales"; function getLocaleFromCookie() { if (typeof document === "undefined") return "en"; const cookie = document.cookie .split(";") .find((c) => c.trim().startsWith(`${LOCALE_COOKIE}=`)); const value = cookie ? decodeURIComponent(cookie.split("=")[1]) : "en"; return normalizeLocale(value); } export default function ProfilePage() { const { theme, setTheme, isDark } = useTheme(); const [locale, setLocale] = useState(() => getLocaleFromCookie()); const [langOpen, setLangOpen] = useState(false); const [shutdownOpen, setShutdownOpen] = useState(false); const [isShuttingDown, setIsShuttingDown] = useState(false); const [settings, setSettings] = useState({ fallbackStrategy: "fill-first" }); const [loading, setLoading] = useState(true); const [passwords, setPasswords] = useState({ current: "", new: "", confirm: "" }); const [passStatus, setPassStatus] = useState({ type: "", message: "" }); const [passLoading, setPassLoading] = useState(false); const [dbLoading, setDbLoading] = useState(false); const [dbStatus, setDbStatus] = useState({ type: "", message: "" }); const [dbAuth, setDbAuth] = useState({ open: false, mode: "", password: "" }); const pendingImportRef = useRef(null); const [oidcForm, setOidcForm] = useState({ authMode: "password", oidcIssuerUrl: "", oidcClientId: "", oidcScopes: "openid profile email", oidcLoginLabel: "Sign in with OIDC", }); const [oidcClientSecret, setOidcClientSecret] = useState(""); const [oidcStatus, setOidcStatus] = useState({ type: "", message: "" }); const [oidcLoading, setOidcLoading] = useState(false); const [oidcTestLoading, setOidcTestLoading] = useState(false); const [oidcTestStatus, setOidcTestStatus] = useState({ type: "", message: "" }); const [oidcExpanded, setOidcExpanded] = useState(false); const origin = typeof window !== "undefined" ? window.location.origin : ""; const oidcRedirectUri = origin ? `${origin}/api/auth/oidc/callback` : "/api/auth/oidc/callback"; const samlAcsUrl = origin ? `${origin}/api/auth/saml/acs` : "/api/auth/saml/acs"; const samlMetadataUrl = origin ? `${origin}/api/auth/saml/metadata` : "/api/auth/saml/metadata"; // SAML State const [ssoTypeTab, setSsoTypeTab] = useState("saml"); const [samlForm, setSamlForm] = useState({ samlEntryPoint: "", samlIssuer: "urn:9router:sp", samlCert: "", samlLoginLabel: "Sign in with SAML SSO", samlAttributeEmail: "email", samlAttributeName: "name", }); const [samlStatus, setSamlStatus] = useState({ type: "", message: "" }); const [samlLoading, setSamlLoading] = useState(false); const [samlTestLoading, setSamlTestLoading] = useState(false); const [samlTestStatus, setSamlTestStatus] = useState({ type: "", message: "" }); const [showSamlGuide, setShowSamlGuide] = useState(false); const idpMetadataFileRef = useRef(null); const certFileRef = useRef(null); const importFileRef = useRef(null); const [proxyForm, setProxyForm] = useState({ outboundProxyEnabled: false, outboundProxyUrl: "", outboundNoProxy: "", }); const [proxyStatus, setProxyStatus] = useState({ type: "", message: "" }); const [proxyLoading, setProxyLoading] = useState(false); const [proxyTestLoading, setProxyTestLoading] = useState(false); useEffect(() => { fetch("/api/settings") .then((res) => res.json()) .then((data) => { setSettings(data); setOidcForm({ authMode: data?.authMode || "password", oidcIssuerUrl: data?.oidcIssuerUrl || "", oidcClientId: data?.oidcClientId || "", oidcScopes: data?.oidcScopes || "openid profile email", oidcLoginLabel: data?.oidcLoginLabel || "Sign in with OIDC", }); setOidcClientSecret(""); setSsoTypeTab(data?.ssoType || "saml"); setSamlForm({ samlEntryPoint: data?.samlEntryPoint || "", samlIssuer: data?.samlIssuer || "urn:9router:sp", samlCert: data?.samlCert || "", samlLoginLabel: data?.samlLoginLabel || "Sign in with SAML SSO", samlAttributeEmail: data?.samlAttributeEmail || "email", samlAttributeName: data?.samlAttributeName || "name", }); if ( data?.authMode === "sso" || data?.authMode === "saml" || data?.authMode === "oidc" || data?.authMode === "both" ) { setOidcExpanded(true); } setProxyForm({ outboundProxyEnabled: data?.outboundProxyEnabled === true, outboundProxyUrl: data?.outboundProxyUrl || "", outboundNoProxy: data?.outboundNoProxy || "", }); setLoading(false); }) .catch((err) => { console.error("Failed to fetch settings:", err); setLoading(false); }); }, []); const updateOutboundProxy = async (e) => { e.preventDefault(); if (settings.outboundProxyEnabled !== true) return; setProxyLoading(true); setProxyStatus({ type: "", message: "" }); try { const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ outboundProxyUrl: proxyForm.outboundProxyUrl, outboundNoProxy: proxyForm.outboundNoProxy, }), }); const data = await res.json(); if (res.ok) { setSettings((prev) => ({ ...prev, ...data })); setProxyStatus({ type: "success", message: "Proxy settings applied" }); } else { setProxyStatus({ type: "error", message: data.error || "Failed to update proxy settings" }); } } catch (err) { setProxyStatus({ type: "error", message: "An error occurred" }); } finally { setProxyLoading(false); } }; const testOutboundProxy = async () => { if (settings.outboundProxyEnabled !== true) return; const proxyUrl = (proxyForm.outboundProxyUrl || "").trim(); if (!proxyUrl) { setProxyStatus({ type: "error", message: "Please enter a Proxy URL to test" }); return; } setProxyTestLoading(true); setProxyStatus({ type: "", message: "" }); try { const res = await fetch("/api/settings/proxy-test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ proxyUrl }), }); const data = await res.json(); if (res.ok && data?.ok) { setProxyStatus({ type: "success", message: `Proxy test OK (${data.status}) in ${data.elapsedMs}ms`, }); } else { setProxyStatus({ type: "error", message: data?.error || "Proxy test failed", }); } } catch (err) { setProxyStatus({ type: "error", message: "An error occurred" }); } finally { setProxyTestLoading(false); } }; const updateOutboundProxyEnabled = async (outboundProxyEnabled) => { setProxyLoading(true); setProxyStatus({ type: "", message: "" }); try { const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ outboundProxyEnabled }), }); const data = await res.json(); if (res.ok) { setSettings((prev) => ({ ...prev, ...data })); setProxyForm((prev) => ({ ...prev, outboundProxyEnabled: data?.outboundProxyEnabled === true })); setProxyStatus({ type: "success", message: outboundProxyEnabled ? "Proxy enabled" : "Proxy disabled", }); } else { setProxyStatus({ type: "error", message: data.error || "Failed to update proxy settings" }); } } catch (err) { setProxyStatus({ type: "error", message: "An error occurred" }); } finally { setProxyLoading(false); } }; const handlePasswordChange = async (e) => { e.preventDefault(); if (passwords.new !== passwords.confirm) { setPassStatus({ type: "error", message: "Passwords do not match" }); return; } setPassLoading(true); setPassStatus({ type: "", message: "" }); try { const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ currentPassword: passwords.current, newPassword: passwords.new, }), }); const data = await res.json(); if (res.ok) { setPassStatus({ type: "success", message: "Password updated successfully" }); setPasswords({ current: "", new: "", confirm: "" }); } else { setPassStatus({ type: "error", message: data.error || "Failed to update password" }); } } catch (err) { setPassStatus({ type: "error", message: "An error occurred" }); } finally { setPassLoading(false); } }; const updateFallbackStrategy = async (strategy) => { try { const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fallbackStrategy: strategy }), }); if (res.ok) { setSettings(prev => ({ ...prev, fallbackStrategy: strategy })); } } catch (err) { console.error("Failed to update settings:", err); } }; const updateComboStrategy = async (strategy) => { try { const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ comboStrategy: strategy }), }); if (res.ok) { setSettings(prev => ({ ...prev, comboStrategy: strategy })); } } catch (err) { console.error("Failed to update combo strategy:", err); } }; const updateStickyLimit = async (limit) => { const numLimit = parseInt(limit); if (isNaN(numLimit) || numLimit < 1) return; try { const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ stickyRoundRobinLimit: numLimit }), }); if (res.ok) { setSettings(prev => ({ ...prev, stickyRoundRobinLimit: numLimit })); } } catch (err) { console.error("Failed to update sticky limit:", err); } }; const updateComboStickyLimit = async (limit) => { const numLimit = parseInt(limit); if (isNaN(numLimit) || numLimit < 1) return; try { const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ comboStickyRoundRobinLimit: numLimit }), }); if (res.ok) { setSettings(prev => ({ ...prev, comboStickyRoundRobinLimit: numLimit })); } } catch (err) { console.error("Failed to update combo sticky limit:", err); } }; const updateRequireLogin = async (requireLogin) => { try { const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ requireLogin }), }); if (res.ok) { setSettings(prev => ({ ...prev, requireLogin })); } } catch (err) { console.error("Failed to update require login:", err); } }; const updateOidcForm = (field, value) => { setOidcForm((prev) => ({ ...prev, [field]: value })); }; const saveOidcSettings = async (authMode = oidcForm.authMode || "password") => { const issuerUrl = oidcForm.oidcIssuerUrl.trim(); const clientId = oidcForm.oidcClientId.trim(); const scopes = oidcForm.oidcScopes.trim(); const loginLabel = oidcForm.oidcLoginLabel.trim(); const secret = oidcClientSecret.trim(); if (authMode !== "password" && (!issuerUrl || !clientId || !secret) && !settings.oidcConfigured) { setOidcStatus({ type: "error", message: "Issuer URL, client ID, and client secret are required to enable OIDC." }); return; } setOidcLoading(true); setOidcStatus({ type: "", message: "" }); setOidcTestStatus({ type: "", message: "" }); try { const payload = { authMode, ssoType: "oidc", oidcIssuerUrl: issuerUrl, oidcClientId: clientId, oidcScopes: scopes || "openid profile email", oidcLoginLabel: loginLabel || "Sign in with OIDC", }; if (secret) { payload.oidcClientSecret = secret; } const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const data = await res.json(); if (res.ok) { setSettings((prev) => ({ ...prev, ...data })); setOidcForm({ authMode: data?.authMode || authMode, oidcIssuerUrl: data?.oidcIssuerUrl || issuerUrl, oidcClientId: data?.oidcClientId || clientId, oidcScopes: data?.oidcScopes || scopes || "openid profile email", oidcLoginLabel: data?.oidcLoginLabel || loginLabel || "Sign in with OIDC", }); setOidcClientSecret(""); setOidcStatus({ type: "success", message: authMode === "oidc" ? "OIDC login enabled" : authMode === "both" ? "Password and OIDC login enabled" : "OIDC settings saved", }); } else { setOidcStatus({ type: "error", message: data.error || "Failed to save OIDC settings" }); } } catch (err) { setOidcStatus({ type: "error", message: "An error occurred" }); } finally { setOidcLoading(false); } }; const testOidcConnection = async () => { const issuerUrl = oidcForm.oidcIssuerUrl.trim(); const clientId = oidcForm.oidcClientId.trim(); const scopes = oidcForm.oidcScopes.trim(); const secret = oidcClientSecret.trim(); if (!issuerUrl || !clientId) { setOidcTestStatus({ type: "error", message: "Issuer URL and client ID are required to test the connection." }); return; } setOidcTestLoading(true); setOidcStatus({ type: "", message: "" }); setOidcTestStatus({ type: "", message: "" }); try { const saveRes = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ authMode: oidcForm.authMode || settings.authMode || "password", oidcIssuerUrl: issuerUrl, oidcClientId: clientId, oidcScopes: scopes || "openid profile email", oidcLoginLabel: oidcForm.oidcLoginLabel.trim() || "Sign in with OIDC", ...(secret ? { oidcClientSecret: secret } : {}), }), }); const saved = await saveRes.json().catch(() => ({})); if (!saveRes.ok) { setOidcTestStatus({ type: "error", message: saved.error || "Failed to save OIDC settings before testing", }); return; } const res = await fetch("/api/auth/oidc/test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ issuerUrl: saved.oidcIssuerUrl || issuerUrl, clientId: saved.oidcClientId || clientId, scopes: saved.oidcScopes || scopes || "openid profile email", }), }); const data = await res.json().catch(() => ({})); if (res.ok && data?.ok) { const statusMessage = data.clientSecretTested ? data.clientSecretValid === true ? `Connection OK. Discovery loaded from ${data.issuerUrl}. Client secret validated too.` : `Connection OK. Discovery loaded from ${data.issuerUrl}. Client secret was not checked.` : `Connection OK. Discovery loaded from ${data.issuerUrl}.`; setOidcTestStatus({ type: "success", message: statusMessage, }); } else { setOidcTestStatus({ type: "error", message: data.error || "OIDC connection test failed" }); } } catch (err) { setOidcTestStatus({ type: "error", message: "An error occurred" }); } finally { setOidcTestLoading(false); } }; const updateSamlForm = (field, value) => { setSamlForm((prev) => ({ ...prev, [field]: value })); }; const handleIdpMetadataUpload = (event) => { const file = event.target.files?.[0]; if (idpMetadataFileRef.current) idpMetadataFileRef.current.value = ""; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const xmlText = e.target?.result || ""; const parser = new DOMParser(); const doc = parser.parseFromString(xmlText, "text/xml"); const parserError = doc.querySelector("parsererror"); if (parserError) { setSamlStatus({ type: "error", message: "Unable to parse valid SAML IdP metadata from XML file" }); return; } const entityID = doc.documentElement.getAttribute("entityID") || ""; const ssoNodes = Array.from(doc.querySelectorAll("SingleSignOnService, *|SingleSignOnService")); let ssoUrl = ""; for (const node of ssoNodes) { const binding = node.getAttribute("Binding") || ""; const location = node.getAttribute("Location") || ""; if (location) { ssoUrl = location; if (binding.includes("HTTP-Redirect")) break; } } const certNodes = Array.from(doc.querySelectorAll("X509Certificate, *|X509Certificate")); let certStr = ""; if (certNodes.length > 0) { certStr = certNodes[0].textContent.trim(); } setSamlForm((prev) => ({ ...prev, samlEntryPoint: ssoUrl || prev.samlEntryPoint, samlIssuer: prev.samlIssuer || "urn:9router:sp", samlCert: certStr || prev.samlCert, })); setSamlStatus({ type: "success", message: `IdP Metadata imported! (SSO URL: ${ssoUrl ? "found" : "not found"}, EntityID: ${entityID ? "found" : "not found"}, Cert: ${certStr ? "found" : "not found"})`, }); } catch (err) { setSamlStatus({ type: "error", message: "Error reading IdP Metadata XML file" }); } }; reader.readAsText(file); }; const handleCertFileUpload = (event) => { const file = event.target.files?.[0]; if (certFileRef.current) certFileRef.current.value = ""; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { const text = e.target?.result || ""; setSamlForm((prev) => ({ ...prev, samlCert: text.trim() })); setSamlStatus({ type: "success", message: "Certificate file loaded into configuration." }); }; reader.readAsText(file); }; const saveSamlSettings = async (targetAuthMode = oidcForm.authMode || "password") => { setSamlLoading(true); setSamlStatus({ type: "", message: "" }); setSamlTestStatus({ type: "", message: "" }); try { const payload = { authMode: targetAuthMode, ssoType: "saml", samlEntryPoint: samlForm.samlEntryPoint.trim(), samlIssuer: samlForm.samlIssuer.trim() || "urn:9router:sp", samlCert: samlForm.samlCert.trim(), samlLoginLabel: samlForm.samlLoginLabel.trim() || "Sign in with SAML SSO", samlAttributeEmail: samlForm.samlAttributeEmail.trim() || "email", samlAttributeName: samlForm.samlAttributeName.trim() || "name", }; const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const data = await res.json(); if (res.ok) { setSettings((prev) => ({ ...prev, ...data })); setSamlForm({ samlEntryPoint: data?.samlEntryPoint || payload.samlEntryPoint, samlIssuer: data?.samlIssuer || payload.samlIssuer, samlCert: data?.samlCert || payload.samlCert, samlLoginLabel: data?.samlLoginLabel || payload.samlLoginLabel, samlAttributeEmail: data?.samlAttributeEmail || payload.samlAttributeEmail, samlAttributeName: data?.samlAttributeName || payload.samlAttributeName, }); setSamlStatus({ type: "success", message: targetAuthMode === "sso" || targetAuthMode === "saml" ? "SAML SSO login enabled" : targetAuthMode === "both" ? "Password and SAML SSO login enabled" : "SAML 2.0 settings saved", }); } else { setSamlStatus({ type: "error", message: data.error || "Failed to save SAML settings" }); } } catch { setSamlStatus({ type: "error", message: "An error occurred while saving SAML settings" }); } finally { setSamlLoading(false); } }; const testSamlConnection = async () => { setSamlTestLoading(true); setSamlStatus({ type: "", message: "" }); setSamlTestStatus({ type: "", message: "" }); try { const res = await fetch("/api/auth/saml/test", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ samlEntryPoint: samlForm.samlEntryPoint.trim(), samlIssuer: samlForm.samlIssuer.trim(), samlCert: samlForm.samlCert.trim(), }), }); const data = await res.json(); if (res.ok && data.ok) { setSamlTestStatus({ type: "success", message: data.message || "SAML configuration verified!" }); } else { setSamlTestStatus({ type: "error", message: data.error || "SAML configuration test failed" }); } } catch { setSamlTestStatus({ type: "error", message: "An error occurred while testing SAML configuration" }); } finally { setSamlTestLoading(false); } }; const updateObservabilityEnabled = async (enabled) => { try { const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enableObservability: enabled }), }); if (res.ok) { setSettings(prev => ({ ...prev, enableObservability: enabled })); } } catch (err) { console.error("Failed to update enableObservability:", err); } }; const reloadSettings = async () => { try { const res = await fetch("/api/settings"); if (!res.ok) return; const data = await res.json(); setSettings(data); } catch (err) { console.error("Failed to reload settings:", err); } }; const handleExportDatabase = async (password) => { setDbLoading(true); setDbStatus({ type: "", message: "" }); try { const res = await fetch("/api/settings/database", { headers: { "x-9r-password": password }, }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || "Failed to export database"); } const payload = await res.json(); const content = JSON.stringify(payload, null, 2); const blob = new Blob([content], { type: "application/json" }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); const stamp = new Date().toISOString().replace(/[.:]/g, "-"); anchor.href = url; anchor.download = `9router-backup-${stamp}.json`; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); URL.revokeObjectURL(url); setDbStatus({ type: "success", message: "Database backup downloaded" }); } catch (err) { setDbStatus({ type: "error", message: err.message || "Failed to export database" }); } finally { setDbLoading(false); } }; const handleImportDatabase = (event) => { const file = event.target.files?.[0]; if (importFileRef.current) importFileRef.current.value = ""; if (!file) return; pendingImportRef.current = file; setDbStatus({ type: "", message: "" }); setDbAuth({ open: true, mode: "import", password: "" }); }; const runImportDatabase = async (password) => { const file = pendingImportRef.current; if (!file) return; setDbLoading(true); try { const raw = await file.text(); const payload = JSON.parse(raw); const res = await fetch("/api/settings/database", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...payload, password }), }); const data = await res.json().catch(() => ({})); if (!res.ok) { throw new Error(data.error || "Failed to import database"); } await reloadSettings(); setDbStatus({ type: "success", message: "Database imported successfully" }); } catch (err) { setDbStatus({ type: "error", message: err.message || "Invalid backup file" }); } finally { pendingImportRef.current = null; setDbLoading(false); } }; // Confirm password modal, then run export or import. const handleDbAuthConfirm = async () => { const { mode, password } = dbAuth; setDbAuth({ open: false, mode: "", password: "" }); if (mode === "export") await handleExportDatabase(password); else if (mode === "import") await runImportDatabase(password); }; const observabilityEnabled = settings.enableObservability === true; const handleShutdown = async () => { setIsShuttingDown(true); try { await fetch("/api/version/shutdown", { method: "POST" }); } catch (e) { // Expected to fail as server shuts down; ignore error } setIsShuttingDown(false); setShutdownOpen(false); }; const handleLogout = async () => { try { const res = await fetch("/api/auth/logout", { method: "POST" }); if (res.ok) { window.location.assign("/login"); } } catch (err) { console.error("Failed to logout:", err); } }; return (
Running on your machine
Database Location
~/.9router/db/data.sqlite
{dbStatus.message}
)}Require login
When ON, dashboard requires password. When OFF, access without login.
Configure enterprise Single Sign-On (SSO) for dashboard access using SAML 2.0 or OIDC.
{/* SSO Protocol Switcher Tabs */}🔑 Required Service Provider (SP) Values for your IdP Setup:
{samlAcsUrl}
{samlForm.samlIssuer || "urn:9router:sp"}
EmailAddress or Unspecified
☁️ AWS IAM Identity Center
{samlAcsUrl}.{samlForm.samlIssuer || "urn:9router:sp"}.Subject or email to ${`{user:email}`}.🔷 Microsoft Entra ID (Azure AD)
{samlForm.samlIssuer || "urn:9router:sp"}{samlAcsUrl}🟢 Okta / Auth0
{samlAcsUrl}{samlForm.samlIssuer || "urn:9router:sp"}🛡️ Keycloak / Authentik
{samlForm.samlIssuer || "urn:9router:sp"}{samlAcsUrl}1-Click IdP Metadata XML Import
Auto-fill SSO URL, Issuer & Cert from XML metadata
ACS Callback URL
{samlAcsUrl}
SP XML Metadata
{samlMetadataUrl}
{samlTestStatus.message}
)} {samlStatus.message && ({samlStatus.message}
)}This value is write-only after saving.
Redirect URI
{oidcRedirectUri}
{oidcTestStatus.message}
)} {oidcStatus.message && ({oidcStatus.message}
)}SSO login ({settings.ssoType === "saml" ? "SAML 2.0" : "OIDC"}) is currently active. Password login is disabled until you switch back.
) : null} {settings.authMode === "both" && (Password and SSO login ({settings.ssoType === "saml" ? "SAML 2.0" : "OIDC"}) are both active.
)}Round Robin
Cycle through accounts to distribute load
Sticky Limit
Calls per account before switching
Combo Round Robin
Cycle through providers in combos instead of always starting with first
Combo Sticky Limit
Calls per combo model before switching
{settings.fallbackStrategy === "round-robin" ? `Currently distributing requests across all available accounts with ${settings.stickyRoundRobinLimit || 3} calls per account.` : "Currently using accounts in priority order (Fill First)."} {settings.comboStrategy === "round-robin" ? ` Combos rotate after ${settings.comboStickyRoundRobinLimit || 1} call${(settings.comboStickyRoundRobinLimit || 1) === 1 ? "" : "s"} per model.` : " Combos always start with their first model."}
Outbound Proxy
Enable proxy for OAuth + provider outbound requests.
{proxyStatus.message}
)}Enable Observability
Record request details for inspection in the logs view
{APP_CONFIG.name} v{APP_CONFIG.version}
Local Mode - All data stored on your machine
Enter your current password to {dbAuth.mode === "export" ? "export" : "import"} the database.
setDbAuth((s) => ({ ...s, password: e.target.value }))} onKeyDown={(e) => { if (e.key === "Enter" && dbAuth.password) handleDbAuthConfirm(); }} placeholder="Current password" autoFocus />