diff --git a/.env b/.env index 5083258..5519a30 100644 --- a/.env +++ b/.env @@ -8,3 +8,9 @@ VITE_APP_TITLE=ADVICOM SQL Manager VITE_USER_ID=1 # Privilegios: READ (solo lectura) | WRITE (permite INSERT, UPDATE, DELETE) VITE_USER_PRIVILEGIO=READ + +# Configuración FTP para Respaldos +VITE_FTP_HOST=ftp://134.122.126.66:21 +VITE_FTP_USER=mysql_inyect +VITE_FTP_PASS=YTJjrRdnCPwjTSrN +VITE_FTP_PATH=/carpeta_respaldos/ \ No newline at end of file diff --git a/.env.example b/.env.example index e4c1257..85dd1c8 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,9 @@ VITE_APP_TITLE=Remote SQL Admin VITE_USER_ID=1 # Privilegios: READ (solo lectura) | WRITE (permite INSERT, UPDATE, DELETE) VITE_USER_PRIVILEGIO=READ + +# Configuración FTP para Respaldos +VITE_FTP_HOST=ftp.tuservidor.com +VITE_FTP_USER=usuario_ftp +VITE_FTP_PASS=clave_ftp +VITE_FTP_PATH=/carpeta_respaldos/ diff --git a/docker-compose.yml b/docker-compose.yml index 07e4395..f11e3aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: context: . dockerfile: Dockerfile args: - - VITE_API_URL=https://ws-sql-inyect.sial.cl:3120/v1 + - VITE_API_URL=https://ws-sql-inyect.sial.cl/v1 - VITE_APP_TITLE=ADVICOM SQL Manager - VITE_USER_ID=1 - VITE_USER_PRIVILEGIO=READ diff --git a/src/App.jsx b/src/App.jsx index 4c53f36..b65e5c7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,11 +1,12 @@ import React, { useState, useEffect } from 'react'; -import { Database, Settings, Menu, Loader2, LogOut, User as UserIcon } from 'lucide-react'; +import { Database, Settings, Menu, Loader2, LogOut, User as UserIcon, Download } from 'lucide-react'; import StoreSelector from './components/StoreSelector'; import SqlEditor from './components/SqlEditor'; import ResultTable from './components/ResultTable'; import CommandHistory from './components/CommandHistory'; import DatabaseSelector from './components/DatabaseSelector'; import Login from './components/Login'; +import BackupModal from './components/BackupModal'; import { commandService, authService } from './services/api'; import { injectDatabaseToSql } from './utils/sqlValidator'; @@ -21,6 +22,7 @@ function App() { const [selectedDatabase, setSelectedDatabase] = useState(null); const [sidebarOpen, setSidebarOpen] = useState(true); const [selectedClient, setSelectedClient] = useState('2'); // Peru is 2, Colombia is 3 + const [isBackupModalOpen, setIsBackupModalOpen] = useState(false); useEffect(() => { // Check if user is already logged in @@ -149,6 +151,84 @@ function App() { }); } }; + const handleBackupRequest = async (backupData) => { + if (!selectedStore) return; + + setIsBackupModalOpen(false); + setExecutionStatus('running'); + setCurrentResult(null); + setPollingProgress(null); + + try { + const now = new Date(); + const dateStr = now.toISOString().split('T')[0]; + const timeStr = now.getTime().toString().slice(-3); + const commandId = `BK-${dateStr}-${timeStr}`; + + const backupSql = `BACKUP ${JSON.stringify({ + database: backupData.database, + ftp: { + host: backupData.host, + user: backupData.user, + pass: backupData.pass, + path: backupData.path + } + })}`; + + // Step 1: Create the command + const createResponse = await commandService.create({ + store_id: parseInt(selectedStore.id, 10), + sql: backupSql, + privilegio: 'CRUD', // As per requirements + id_user: currentUser.id, + id_cliente: parseInt(selectedClient, 10), + command_id: commandId, + timeout: 600 + }); + + if (!createResponse.success) { + setExecutionStatus('error'); + setCurrentResult({ error: createResponse.message || 'Error al solicitar respaldo' }); + return; + } + + // Update status to polling + setExecutionStatus('polling'); + setCurrentResult({ + message: 'Solicitud de respaldo enviada. El agente está procesando...', + command_id: commandId, + stats: { info: `Backup #${commandId} iniciado. Esperando compresión y envío...` } + }); + + // Step 2: Poll for results + const result = await commandService.pollForResult(commandId, { + interval: 5000, // Poll every 5 seconds for backups + maxAttempts: 120, // Wait up to 10 minutes (600 seconds) + onProgress: ({ attempts, maxAttempts, status }) => { + setPollingProgress({ attempts, maxAttempts, status }); + setCurrentResult(prev => ({ + ...prev, + message: `Procesando respaldo remoto... (${attempts}/${maxAttempts})`, + stats: { info: `Backup #${commandId} - Intento ${attempts} de ${maxAttempts}` } + })); + } + }); + + setExecutionStatus('completed'); + setPollingProgress(null); + setCurrentResult(normalizeApiResponse(result, commandId)); + + } catch (error) { + console.error(error); + setExecutionStatus('error'); + setPollingProgress(null); + setCurrentResult({ + error: error.message || "Error al procesar el respaldo", + stats: { info: 'Error durante la ejecución del backup' } + }); + } + }; + /** * Normalizes different API response formats into a standard structure for ResultTable @@ -174,6 +254,17 @@ function App() { // Check for nested result object (main format) or json_response (history format) const dataContainer = result.result || result.json_response; + // Check for download URL in dataContainer + if (dataContainer && (dataContainer.download_url || dataContainer.url || dataContainer.file_url)) { + normalizedResult.downloadUrl = dataContainer.download_url || dataContainer.url || dataContainer.file_url; + normalizedResult.message = '¡Respaldo listo para descargar!'; + } + // Fallback search in the whole result object + else if (result.download_url || result.url || result.file_url) { + normalizedResult.downloadUrl = result.download_url || result.url || result.file_url; + normalizedResult.message = '¡Respaldo listo para descargar!'; + } + if (dataContainer) { // Get execution time if (dataContainer.execution_time_ms) { @@ -330,6 +421,16 @@ function App() {
+ El archivo se encuentra disponible en el servidor FTP especificado. +
+