"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 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 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 updateObservabilitySetting = async (key, value) => { const numValue = parseInt(value); if (isNaN(numValue) || numValue < 1) return; try { const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ [key]: numValue }), }); if (res.ok) { setSettings(prev => ({ ...prev, [key]: numValue })); } } catch (err) { console.error(`Failed to update ${key}:`, err); } }; const updateObservabilityEnabled = async (enabled) => { try { const res = await fetch("/api/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ observabilityEnabled: enabled }), }); if (res.ok) { setSettings(prev => ({ ...prev, observabilityEnabled: enabled })); } } catch (err) { console.error("Failed to update observabilityEnabled:", 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.observabilityEnabled !== false; return (
Running on your machine
All data is stored locally in the ~/.9router/db.json file.
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
{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)."}
Outbound Proxy
Enable proxy for OAuth + provider outbound requests.
{proxyStatus.message}
)}Dark Mode
Switch between light and dark themes
Database Location
~/.9router/db.json
{dbStatus.message}
)}Enable Observability
Turn request detail recording on/off globally
Max Records
Maximum request detail records to keep (older records are auto-deleted)
Batch Size
Number of items to accumulate before writing to database (higher = better performance)
Flush Interval (ms)
Maximum time to wait before flushing buffer (prevents data loss during low traffic)
Max JSON Size (KB)
Maximum size for each JSON field (request/response) before truncation
Current: Keeps {settings.observabilityMaxRecords || 1000} records, batches every {settings.observabilityBatchSize || 20} requests, max {settings.observabilityMaxJsonSize || 1024}KB per field
{APP_CONFIG.name} v{APP_CONFIG.version}
Local Mode - All data stored on your machine