"use client"; import { useState, useEffect, useRef } from "react"; import { Card, Button, Toggle, Input } from "@/shared/components"; import { useTheme } from "@/shared/hooks/useTheme"; import { cn } from "@/shared/utils/cn"; import { APP_CONFIG } from "@/shared/constants/config"; export default function ProfilePage() { const { theme, setTheme, isDark } = useTheme(); 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 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); 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 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 () => { setDbLoading(true); setDbStatus({ type: "", message: "" }); try { const res = await fetch("/api/settings/database"); 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 = async (event) => { const file = event.target.files?.[0]; if (!file) return; setDbLoading(true); setDbStatus({ type: "", message: "" }); 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), }); 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 { if (importFileRef.current) { importFileRef.current.value = ""; } setDbLoading(false); } }; const observabilityEnabled = settings.enableObservability === true; 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.
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