diff --git a/app/(main)/settings/page.tsx b/app/(main)/settings/page.tsx index c7f870df..55b94647 100644 --- a/app/(main)/settings/page.tsx +++ b/app/(main)/settings/page.tsx @@ -1,17 +1,15 @@ "use client" -import { useCallback, useEffect, useState } from "react" +import { useCallback, useState } from "react" import Link from "next/link" import { PageHeader } from "@/components/page-header" -import { FormToggle } from "@/components/form-kit" +import { useTheme } from "@/components/theme-provider" import { FileImportDialog } from "@/components/file-import-dialog" import { EvoBgpSettingsSection } from "@/components/settings/evobgp-settings-section" import { OpsPanel } from "@/components/ops-panel" -import { Frame, FramePanel } from "@/components/reui/frame" import { Button, buttonVariants } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { useDataSource } from "@/lib/data-source" -import { DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS } from "@/lib/route-optimizer-data" import { AlertDialog, AlertDialogAction, @@ -24,35 +22,26 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog" import { - SaveIcon, TrashIcon, PlusIcon, CheckIcon, CopyIcon, + CheckIcon, AlertCircleIcon, - RefreshCwIcon, LoaderCircleIcon, + LoaderCircleIcon, DownloadIcon, UploadIcon, XIcon, } from "lucide-react" import { cn } from "@/lib/utils" import { downloadSystemDatabaseBackup, restoreSystemDatabaseBackup } from "@/shared/api/system-database" import { toast } from "sonner" -interface ApiKey { id: string; name: string; prefix: string; created: string; last: string; scopes: string[] } - - -const INIT_API_KEYS: ApiKey[] = [ - { id: "k1", name: "Monitoring Script", prefix: "rl_live_mU9k2…", created: "12 дн назад", last: "2м назад", scopes: ["servers:read", "probes:read"] }, - { id: "k2", name: "CI/CD deploy hook", prefix: "rl_live_vB3p8…", created: "1 мес назад", last: "вчера", scopes: ["filters:write", "domains:write"] }, - { id: "k3", name: "Grafana datasource", prefix: "rl_live_xK7r1…", created: "2 мес назад", last: "сейчас", scopes: ["traffic:read", "servers:read"] }, -] - const SECTIONS_NAV = ["Общие", "EvoBGP", "Уведомления", "API-ключи", "Безопасность"] as const type NavSection = typeof SECTIONS_NAV[number] function SettingRow({ label, description, children }: { label: string; description?: string; children: React.ReactNode }) { return ( -
+

{label}

{description &&

{description}

}
-
{children}
+
{children}
) } @@ -103,66 +92,13 @@ function DatabaseRestoreConfirm({ export default function SettingsPage() { const [section, setSection] = useState("Общие") - const [saved, setSaved] = useState(false) - const [copied, setCopied] = useState(null) - - // data source - const { mode, setMode, backendUrl, setBackendUrl, backendUrlLocked, mockModeAvailable, backendStatus, checkBackend } = useDataSource() - const [urlDraft, setUrlDraft] = useState(backendUrl) - - useEffect(() => { - setUrlDraft(backendUrl) - }, [backendUrl]) - - const commitBackendUrl = useCallback(() => { - const normalized = urlDraft.trim().replace(/\/$/, "") - setUrlDraft(normalized) - setBackendUrl(normalized) - void checkBackend() - }, [urlDraft, setBackendUrl, checkBackend]) - // general - const [lang, setLang] = useState("ru") - const [theme, setTheme] = useState("system") - const [timezone, setTimezone] = useState("Europe/Moscow") - const [refreshSec, setRefreshSec] = useState("30") - const [probeTimeout, setProbeTimeout] = useState("5") + const { mode, setMode, backendUrl, backendUrlLocked, mockModeAvailable, backendStatus, checkBackend } = useDataSource() + const { theme, setTheme } = useTheme() const [dbBackupBusy, setDbBackupBusy] = useState(false) const [dbRestoreBusy, setDbRestoreBusy] = useState(false) const [dbRestoreFile, setDbRestoreFile] = useState(null) const [dbRestoreDialogOpen, setDbRestoreDialogOpen] = useState(false) - // notifications - const [notifEmail, setNotifEmail] = useState(true) - const [notifSlack, setNotifSlack] = useState(false) - const [notifWh, setNotifWh] = useState(true) - const [notifDegr, setNotifDegr] = useState(true) - const [notifOffline, setNotifOffline] = useState(true) - const [notifBgp, setNotifBgp] = useState(true) - const [notifBackup, setNotifBackup] = useState(false) - - // api keys - const [apiKeys, setApiKeys] = useState(INIT_API_KEYS) - - // security - const [mfa, setMfa] = useState(true) - const [sessMin, setSessMin] = useState("480") - const [ipAllow, setIpAllow] = useState("10.0.0.0/8\n192.168.0.0/16") - const [auditLog, setAuditLog] = useState(true) - - const handleCopy = (text: string) => { - navigator.clipboard.writeText(text).catch(() => {}) - setCopied(text); setTimeout(() => setCopied(null), 1500) - toast.success("Скопировано в буфер обмена") - } - - const markSaved = useCallback(() => { - setSaved(true) - toast.success("Настройки сохранены") - setTimeout(() => setSaved(false), 2000) - }, []) - - const handleSave = markSaved - const systemDbAvailable = mode === "live" && backendStatus === true const handleSystemDatabaseBackup = useCallback(async () => { @@ -201,7 +137,6 @@ export default function SettingsPage() { const renderContent = () => { - const ra = DEFAULT_ROUTE_AI_OPTIMIZER_SETTINGS // ── Общие ── if (section === "Общие") return ( @@ -212,18 +147,18 @@ export default function SettingsPage() { title="Источник данных" description={ mockModeAvailable - ? "Переключите между живыми данными от бекенда и суррогатными моками из lib/data.ts" - : "В продакшен-образе доступны только живые данные от бекенда" + ? "Выберите живые или демонстрационные данные. Режим применяется сразу в этом браузере." + : "Источник живых данных задан конфигурацией приложения." } contentClassName="divide-y px-5" > {/* mode toggle */} {mockModeAvailable ? ( - +
{([["mock", "Моковые"], ["live", "Живые"]] as const).map(([v, l], i) => ( - -
+
{/* connection status */} @@ -306,47 +221,18 @@ export default function SettingsPage() { {mode === "live" && backendStatus === false && (
- Бекенд недоступен — данные будут отображаться из кеша или моков. Запустите бекенд: cd backend && npm run dev + Сервер недоступен. Последние полученные данные сохраняются; новые запросы могут завершаться ошибкой.
)} - {/* ── Основные настройки ── */} - - - - - -
- {[["light","Светлая"],["dark","Тёмная"],["system","Авто"]].map(([v, l]) => ( - - ))} -
-
- - - - - setRefreshSec(e.target.value)} /> - - - setProbeTimeout(e.target.value)} /> - + + Русский +
+ {([["light","Светлая"],["dark","Тёмная"],["system","Авто"]] as const).map(([value,label]) => )} +
+

Периоды сбора данных и параметры диагностики настраиваются в соответствующих разделах.

+
Сбор данныхДиагностика
- -
-
-
Мин. выигрыш (переключение)
-
{ra.switchThreshold}%
-
-
-
Гистерезис
-
{ra.hysteresisThreshold}%
-
-
-
Вес задержки (ping)
-
{ra.pingWeight}%
-
-
-
Интервал зондирования
-
{ra.probeIntervalMin} мин
-
-
-
Автоприменение
-
{ra.autoApply ? `да, каждые ${ra.autoApplyIntervalMin} мин` : "нет"}
-
-
- - Открыть оптимизатор маршрутов - -
+ Открыть оптимизатор
) if (section === "EvoBGP") return - // ── Уведомления ── - if (section === "Уведомления") return ( -
- - - - - {notifEmail &&
} - - - - {notifSlack &&
} - - - - {notifWh &&
} -
- - - - - - - - - - - - - - -
- ) - - // ── API-ключи ── - if (section === "API-ключи") return ( -
-
- -
-
- {apiKeys.map(k => ( - - -
-
-

{k.name}

-
- {k.prefix} - -
-
- {k.scopes.map(s => ( - {s} - ))} -
-
-
-

Создан: {k.created}

-

Использован: {k.last}

-
- - -
-
-
-
- - ))} -
- -
- {[ - ["GET", "/servers", "Список серверов"], - ["GET", "/traffic", "Данные трафика"], - ["GET", "/filters", "Фильтры маршрутов"], - ["POST", "/filters", "Создать фильтр"], - ["GET", "/probes", "Диагностические пробы"], - ["POST", "/probes/run", "Запустить тест"], - ].map(([method, path, desc], i) => ( -
- {method} - {path} - {desc} -
- ))} -
-
-
- ) - - // ── Безопасность ── - if (section === "Безопасность") return ( -
- - - - - - setSessMin(e.target.value)} /> - - - -