"use client" import {useEffect,useRef,useState} from "react" import Link from "next/link" import {useDataSource} from "@/lib/data-source" import {can} from "@/lib/auth" import {ApiClientError} from "@/shared/api/http-client" import {downloadSystemDatabaseBackup,restoreSystemDatabaseBackup} from "@/shared/api/system-database" import {OpsPanel} from "@/components/ops-panel" import {Button,buttonVariants} from "@/components/ui/button" import {FileImportDialog} from "@/components/file-import-dialog" import {AlertDialog,AlertDialogContent,AlertDialogHeader,AlertDialogTitle,AlertDialogDescription,AlertDialogFooter,AlertDialogCancel,AlertDialogAction} from "@/components/ui/alert-dialog" import {DownloadIcon,UploadIcon,LoaderCircleIcon} from "lucide-react" const pending=new Set() function storageKey(source:string){return "mm:database-restore-pending:"+source} function unresolved(source:string){try{return pending.has(source)||sessionStorage.getItem(storageKey(source))==="true"}catch{return pending.has(source)}} function checkpoint(source:string,value:boolean){if(value)pending.add(source);else pending.delete(source);try{if(value)sessionStorage.setItem(storageKey(source),"true");else sessionStorage.removeItem(storageKey(source))}catch{/* Memory guard still survives section navigation. */}} export function SystemDatabaseSection(){const {mode,backendUrl}=useDataSource();const source=JSON.stringify([mode,backendUrl]);return } function DatabaseForm({source}:{source:string}){ const {mode,backendUrl,backendStatus}=useDataSource() const [busy,setBusy]=useState<"backup"|"restore"|null>(null),[file,setFile]=useState(null),[importOpen,setImportOpen]=useState(false) const [unknown,setUnknown]=useState(false),[error,setError]=useState(null),[outcome,setOutcome]=useState(null),[warnings,setWarnings]=useState([]) const [ready,setReady]=useState(false) const active=useRef(true),lock=useRef(false),controller=useRef(null) useEffect(()=>{active.current=true;queueMicrotask(()=>{if(active.current){setUnknown(unresolved(source));setReady(true)}});return()=>{active.current=false;controller.current?.abort()}},[source]) const allowed=can("mm:settings:admin"),available=ready&&mode==="live"&&backendStatus===true&&allowed function current(){return active.current&&can("mm:settings:admin")} function begin(kind:"backup"|"restore"){ if(!available||lock.current||!current()||(kind==="restore"&&unknown))return null lock.current=true;controller.current=new AbortController();setBusy(kind);setError(null);setOutcome(null);setWarnings([]);return controller.current.signal } function finish(){lock.current=false;controller.current=null;if(current())setBusy(null)} async function backup(){ const signal=begin("backup");if(!signal)return try{ const result=await downloadSystemDatabaseBackup(backendUrl,{signal});if(!current())return const url=URL.createObjectURL(result.blob) try{const a=document.createElement("a");a.href=url;a.download=result.filename;a.click()}finally{URL.revokeObjectURL(url)} setOutcome("Бэкап базы приложения подготовлен к скачиванию");setWarnings(Object.values(result.warnings)) }catch(e){if(current())setError(e instanceof Error?e.message:"Не удалось подготовить бэкап")} finally{finish()} } async function restore(){ if(!file)return const signal=begin("restore");if(!signal)return let sent=false try{ const receipt=await restoreSystemDatabaseBackup(backendUrl,file,{signal,onDispatch:()=>{if(!current())throw Error("Источник изменился");sent=true;checkpoint(source,true);setUnknown(true)}}) if(!current())return;checkpoint(source,false) setUnknown(false);setFile(null);setOutcome("База приложения восстановлена");setWarnings([receipt.eventError,receipt.listenerError,receipt.schedulerError,receipt.cleanupError,receipt.maintenanceError,receipt.migrationError].filter((value):value is string=>!!value)) }catch(e){ const rejected=e instanceof ApiClientError&&[401,403,409,413].includes(e.status) if(sent&&rejected&¤t())checkpoint(source,false) if(current()){setUnknown(sent&&!rejected);const message=e instanceof Error?e.message:"Результат восстановления не получен";setError(message)} }finally{finish()} } const messages=<> {error?

{error}

:null} {outcome?

{outcome}

:null} {warnings.map((warning,index)=>

{warning}

)} {unknown?

Результат восстановления неизвестен. Запрос мог выполниться на сервере. До повтора проверьте данные и журнал операций; проверка доступности сервера этого не подтверждает.

:null} return
{!available?

{!allowed?"Нужны права администратора настроек.":mode!=="live"?"Операции базы доступны в live-режиме.":"Доступность сервера не подтверждена."}

:null}

Скачать бэкап

Дамп PostgreSQL в формате custom (.dump)

Восстановить из файла

Полностью заменяет текущие данные. Максимум 512 МБ.

{!file?messages:null}

Сбросить данные NetFlow

Проверка и подтверждение — в разделе сбора данных

Открыть NetFlow
{if(!available||lock.current||unknown)return;setFile(files[0]??null);setError(null);setOutcome(null);setWarnings([])}}/> {if(!open&&!lock.current)setFile(null)}}> Восстановить базу приложения?Файл: {file?.name}. Текущие данные PostgreSQL будут полностью заменены. Источник: {backendUrl||"текущий сервер"}. Рекомендуется сначала скачать актуальный бэкап.{file?messages:null}setFile(null)}>Отмена{e.preventDefault();void restore()}}>{busy==="restore"?:null}{busy==="restore"?"Восстановление…":"Восстановить"}
}