"use client" import { useCallback, useState } from "react" import Link from "next/link" import { PageHeader } from "@/components/page-header" 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 { Button, buttonVariants } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { useDataSource } from "@/lib/data-source" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { CheckIcon, AlertCircleIcon, LoaderCircleIcon, DownloadIcon, UploadIcon, XIcon, } from "lucide-react" import { cn } from "@/lib/utils" import { downloadSystemDatabaseBackup, restoreSystemDatabaseBackup } from "@/shared/api/system-database" import { toast } from "sonner" 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}
) } function DatabaseRestoreConfirm({ open, filename, busy, onConfirm, onCancel, }: { open: boolean filename: string busy?: boolean onConfirm: () => void onCancel: () => void }) { return ( { if (!v && !busy) onCancel() }}> Восстановить базу приложения? Файл: {filename}. Текущие данные PostgreSQL на бекенде будут полностью заменены. Рекомендуется сначала скачать актуальный бэкап. Отмена {busy ? : null} {busy ? "Восстановление…" : "Восстановить"} ) } // ─── page ───────────────────────────────────────────────────────────────────── export default function SettingsPage() { const [section, setSection] = useState("Общие") 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) const systemDbAvailable = mode === "live" && backendStatus === true const handleSystemDatabaseBackup = useCallback(async () => { if (!systemDbAvailable) return setDbBackupBusy(true) try { const { blob, filename } = await downloadSystemDatabaseBackup(backendUrl) const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = filename a.click() URL.revokeObjectURL(url) toast.success("Бэкап базы приложения скачан") } catch (e) { toast.error(e instanceof Error ? e.message : "Не удалось создать бэкап") } finally { setDbBackupBusy(false) } }, [backendUrl, systemDbAvailable]) const handleSystemDatabaseRestoreConfirm = useCallback(async () => { if (!dbRestoreFile || !systemDbAvailable) return setDbRestoreBusy(true) try { await restoreSystemDatabaseBackup(backendUrl, dbRestoreFile) toast.success("База приложения восстановлена") setDbRestoreFile(null) } catch (e) { toast.error(e instanceof Error ? e.message : "Не удалось восстановить базу") } finally { setDbRestoreBusy(false) } }, [backendUrl, dbRestoreFile, systemDbAvailable]) const renderContent = () => { // ── Общие ── if (section === "Общие") return (
{/* ── Источник данных ── */} {/* mode toggle */} {mockModeAvailable ? (
{([["mock", "Моковые"], ["live", "Живые"]] as const).map(([v, l], i) => ( ))}
) : ( Живые )} {/* backend URL — visible in both modes so user can configure before switching */} {/* connection status */}
{backendStatus === undefined && ( Не проверялось )} {backendStatus === true && ( Доступен )} {backendStatus === false && ( Недоступен )}
{/* hint when live but backend unreachable */} {mode === "live" && backendStatus === false && (
Сервер недоступен. Последние полученные данные сохраняются; новые запросы могут завершаться ошибкой.
)}
Русский
{([["light","Светлая"],["dark","Тёмная"],["system","Авто"]] as const).map(([value,label]) => )}

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

Сбор данныхДиагностика
{!systemDbAvailable && (
Доступно только в live-режиме при доступном бекенде.
)}
{dbRestoreFile && (

{dbRestoreFile.name}

)}
{systemDbAvailable ? Открыть NetFlow : }
Открыть оптимизатор
) if (section === "EvoBGP") return if (section === "Уведомления") return Открыть оповещения if (section === "API-ключи") return

Создание и управление отдельными API-ключами в приложении пока недоступны.

return

Состояние MFA, ограничений IP и настроек аудита в этом разделе не получено. Изменение этих параметров здесь недоступно.

Управление пользователями
} return (
{/* nav */} {/* content */}
{renderContent()}
{/* delete confirm */} {dbRestoreFile && ( { void handleSystemDatabaseRestoreConfirm() }} onCancel={() => { if (dbRestoreBusy) return setDbRestoreFile(null) }} /> )} { const file = files[0] if (!file) return setDbRestoreFile(file) }} />
) } function BackendUrlField(){ const {backendUrl,setBackendUrl,backendUrlLocked}=useDataSource() const [draft,setDraft]=useState(backendUrl),[error,setError]=useState(null) function commit(){ if(backendUrlLocked||draft===backendUrl)return try{const url=new URL(draft.trim());if(!["http:","https:"].includes(url.protocol)||url.username||url.password||url.search||url.hash)throw Error();setBackendUrl(url.toString().replace(/\/$/,""));setError(null)} catch{setError("Укажите HTTP(S) адрес без учётных данных и параметров")} } return
{setDraft(e.target.value);setError(null)}} onBlur={commit} onKeyDown={e=>{if(e.key==="Enter"){e.preventDefault();commit()}}} readOnly={backendUrlLocked} aria-invalid={!!error} placeholder="http://localhost:8000"/>{error?

{error}

:null}
}