const API_BASE = ""; // ============================================================================ // تعريف دوال التدفق (Stream) محلياً - حل المشكلة // ============================================================================ function createProgressStream(migrationId, type = 's3', options = {}) { const { onProgress = () => {}, onComplete = () => {}, onError = () => {}, reconnectInterval = 3000, maxReconnectAttempts = 5 } = options; let eventSource = null; let reconnectAttempts = 0; let isActive = true; let reconnectTimer = null; const connect = () => { if (!isActive) return; if (eventSource) { eventSource.close(); } try { const safeMigrationId = encodeURIComponent(migrationId); const safeType = encodeURIComponent(type); const url = `${API_BASE}/api/stream-progress/${safeMigrationId}?type=${safeType}`; eventSource = new EventSource(url); eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); // التحقق الموحد من الإكمال if (data.type === 'completion' || data.status === 'completed' || data.status === 'success' || data.success === true) { onComplete(data); if (eventSource) eventSource.close(); } // التحقق الموحد من الخطأ else if (data.type === 'error' || data.status === 'failed' || data.status === 'error' || data.error) { onError(data); if (eventSource) eventSource.close(); } else { onProgress(data); } } catch (error) { console.error('Error parsing stream data:', error); } }; eventSource.onerror = (error) => { console.error('EventSource error:', error); if (isActive && reconnectAttempts < maxReconnectAttempts) { reconnectAttempts++; console.log(`Reconnecting... Attempt ${reconnectAttempts}/${maxReconnectAttempts}`); if (reconnectTimer) clearTimeout(reconnectTimer); reconnectTimer = setTimeout(connect, reconnectInterval); } else if (reconnectAttempts >= maxReconnectAttempts) { onError({ error: 'Max reconnection attempts reached' }); } if (eventSource) { eventSource.close(); eventSource = null; } }; eventSource.onopen = () => { console.log(`Stream connected for ${migrationId}`); reconnectAttempts = 0; }; } catch (error) { console.error('Error creating EventSource:', error); onError({ error: error.message }); } }; connect(); return { stop: () => { isActive = false; if (reconnectTimer) clearTimeout(reconnectTimer); if (eventSource) { eventSource.close(); eventSource = null; } }, pause: () => { isActive = false; if (reconnectTimer) clearTimeout(reconnectTimer); if (eventSource) { eventSource.close(); eventSource = null; } }, resume: () => { if (!isActive) { isActive = true; reconnectAttempts = 0; connect(); } } }; } function formatProgressDisplay(progress) { if (!progress) return 'Connecting...'; const lines = []; if (progress.migration_id) { lines.push(`🚀 Migration: ${progress.migration_id}`); } if (progress.status) { const statusEmoji = { 'running': '🟢', 'completed': '✅', 'failed': '❌', 'cancelled': '⏹️', 'success': '✅', 'error': '❌' }[progress.status] || '📊'; lines.push(`${statusEmoji} Status: ${progress.status}`); } if (progress.percentage !== undefined) { lines.push(`📊 Progress: ${progress.percentage.toFixed(1)}%`); } if (progress.processed_objects !== undefined && progress.total_objects !== undefined) { lines.push(`📦 Objects: ${progress.processed_objects}/${progress.total_objects}`); } if (progress.processed?.tables !== undefined && progress.total?.tables !== undefined) { lines.push(`📋 Tables: ${progress.processed.tables}/${progress.total.tables}`); } if (progress.processed_size_formatted && progress.total_size_formatted) { lines.push(`💾 Size: ${progress.processed_size_formatted}/${progress.total_size_formatted}`); } if (progress.current_speed_formatted) { lines.push(`⚡ Speed: ${progress.current_speed_formatted}`); } if (progress.elapsed_time_formatted) { lines.push(`⏱️ Elapsed: ${progress.elapsed_time_formatted}`); } if (progress.eta_formatted) { lines.push(`⏳ ETA: ${progress.eta_formatted}`); } if (progress.current_object) { const objName = progress.current_object.length > 50 ? '...' + progress.current_object.slice(-50) : progress.current_object; lines.push(`📄 Current: ${objName}`); } return lines.join('\n'); } // Copy to clipboard async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); return true; } catch (err) { console.error('Failed to copy:', err); return false; } } // Show notification function showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.innerHTML = ` ${type === 'success' ? '✅' : type === 'error' ? '❌' : type === 'warning' ? '⚠️' : 'ℹ️'} ${message} `; document.body.appendChild(notification); setTimeout(() => { if (notification.parentElement) { notification.style.animation = 'slideOut 0.3s ease'; setTimeout(() => notification.remove(), 300); } }, 3000); } // Format environment variables function formatEnvVars(envVars, format = 'dotenv') { if (!envVars) return ''; switch(format) { case 'dotenv': return Object.entries(envVars) .map(([key, value]) => `${key}=${value}`) .join('\n'); case 'json': return JSON.stringify(envVars, null, 2); case 'shell_export': return Object.entries(envVars) .map(([key, value]) => `export ${key}="${value}"`) .join('\n'); case 'docker_env': return Object.entries(envVars) .map(([key, value]) => `-e ${key}="${value}"`) .join(' '); case 'docker_compose': return Object.entries(envVars) .map(([key, value]) => ` ${key}: ${value}`) .join('\n'); default: return Object.entries(envVars) .map(([key, value]) => `${key}: ${value}`) .join('\n'); } } // Format file size function formatFileSize(bytes) { if (bytes === 0 || !bytes) return '0 Bytes'; if (typeof bytes !== 'number') bytes = parseInt(bytes); const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // Format PostgreSQL configuration for display function formatPostgresConfig(config) { if (!config) return ''; const details = [ `📍 PostgreSQL Configuration:`, `━━━━━━━━━━━━━━━━━━━━━━━━━━━`, `Host: ${config.host || 'Not set'}`, `Port: ${config.port || 5432}`, `User: ${config.user || 'Not set'}`, `Database: ${config.database || 'Not set'}`, `━━━━━━━━━━━━━━━━━━━━━━━━━━━` ]; if (config.host && config.user && config.password) { details.push(`✅ Credentials: Configured`); } else { details.push(`❌ Credentials: Not configured`); } return details.join('\n'); } // Format S3 configuration for display function formatS3Config(config, type = 'source') { if (!config) return ''; const details = [ `📍 ${type.toUpperCase()} S3 Configuration:`, `━━━━━━━━━━━━━━━━━━━━━━━━━━━`, `Endpoint: ${config.endpoint_url || 'AWS S3 (default)'}`, `Region: ${config.region || 'us-east-1'}`, `Bucket: ${config.bucket || 'Not set'}`, `━━━━━━━━━━━━━━━━━━━━━━━━━━━` ]; if (config.access_key_id && config.secret_access_key) { details.push(`✅ Credentials: Configured`); } else { details.push(`❌ Credentials: Not configured`); } return details.join('\n'); } // Format migration details for display function formatMigrationDetails(migrationData) { if (!migrationData) return ''; const details = [ `🔄 Migration ID: ${migrationData.migration_id || migrationData.id || 'N/A'}`, `━━━━━━━━━━━━━━━━━━━━━━━━━━━`, `Status: ${migrationData.status || 'N/A'}`, `Started: ${migrationData.started_at ? new Date(migrationData.started_at * 1000).toLocaleString() : 'N/A'}` ]; if (migrationData.stats) { details.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━`); details.push(`📊 Statistics:`); Object.entries(migrationData.stats).forEach(([key, value]) => { details.push(` ${key}: ${value}`); }); } if (migrationData.message) { details.push(`━━━━━━━━━━━━━━━━━━━━━━━━━━━`); details.push(`📝 Message: ${migrationData.message}`); } return details.join('\n'); } // Extract PostgreSQL info from URI function extractPostgresInfo(uri) { try { const match = uri.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/); if (match) { return { user: match[1], password: match[2], host: match[3], port: parseInt(match[4]), database: match[5], isValid: true }; } return { isValid: false }; } catch (error) { console.error('Error extracting PostgreSQL info:', error); return { isValid: false, error: error.message }; } } // Extract S3 info from URI function extractS3Info(s3Uri) { try { if (s3Uri.startsWith('s3://')) { const uri = s3Uri.substring(5); const parts = uri.split('/', 1); const bucket = parts[0]; const key = uri.substring(bucket.length + 1); return { bucket, key: key || '', fullUri: s3Uri, isValid: true }; } return { isValid: false }; } catch (error) { console.error('Error parsing S3 URI:', error); return { isValid: false, error: error.message }; } } // Build PostgreSQL connection string function buildPostgresConnectionString(host, database, user, port = 5432) { if (!host || !database) return ''; return `postgresql://${user ? user + '@' : ''}${host}:${port}/${database}`; } // Build S3 URL function buildS3Url(bucket, key, endpointUrl = null) { if (!bucket) return ''; if (!key) return `s3://${bucket}`; if (endpointUrl) { return `${endpointUrl}/${bucket}/${key}`; } return `s3://${bucket}/${key}`; } // Group tables by schema function groupTablesBySchema(tables) { const groups = {}; tables.forEach(table => { const schema = table.schema || 'public'; if (!groups[schema]) { groups[schema] = { count: 0, tables: [] }; } groups[schema].count++; groups[schema].tables.push(table); }); return groups; } // Group objects by prefix function groupObjectsByPrefix(objects, depth = 1) { const groups = {}; objects.forEach(obj => { const parts = obj.key.split('/'); let prefix = ''; for (let i = 0; i < Math.min(depth, parts.length - 1); i++) { prefix += parts[i] + '/'; } if (!prefix) prefix = '/'; if (!groups[prefix]) { groups[prefix] = { count: 0, totalSize: 0, objects: [] }; } groups[prefix].count++; groups[prefix].totalSize += obj.size; groups[prefix].objects.push(obj); }); return groups; } // Estimate migration time function estimateMigrationTime(totalItems, averageItemsPerSecond = 100) { if (!totalItems) return 'Unknown'; const seconds = totalItems / averageItemsPerSecond; if (seconds < 60) return `${Math.ceil(seconds)} seconds`; if (seconds < 3600) return `${Math.ceil(seconds / 60)} minutes`; if (seconds < 86400) return `${(seconds / 3600).toFixed(1)} hours`; return `${(seconds / 86400).toFixed(1)} days`; } // ============================================================================ // PostgreSQL API Functions // ============================================================================ // Test PostgreSQL connection async function testPostgresConnection(options = {}) { const { useEnvVars = false, uri, host, user, password, database, port = 5432 } = options; const body = { use_env_vars: useEnvVars }; if (uri) { body.uri = uri; } else if (host && user && password && database) { body.uri = `postgresql://${user}:${password}@${host}:${port}/${database}`; } const r = await fetch(`${API_BASE}/api/postgres/test-connection`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); return r.json(); } // Get PostgreSQL schemas async function getPostgresSchemas(options = {}) { const { useEnvVars = false, uri, host, user, password, database, port = 5432 } = options; const body = { use_env_vars: useEnvVars }; // بناء الرابط إذا لم يتم توفيره if (uri) { body.uri = uri; } else if (host && user && password && database) { body.uri = `postgresql://${user}:${password}@${host}:${port}/${database}`; } const r = await fetch(`${API_BASE}/api/postgres/get-schemas`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) // ✅ الآن يرسل الجسم كاملاً }); return r.json(); } // Get PostgreSQL tables async function getPostgresTables(options = {}){ const { useEnvVars = false, uri, host, user, password, database, port = 5432, schema = '' } = options; const body = { use_env_vars: useEnvVars }; // بناء الرابط إذا لم يتم توفيره if (uri) { body.uri = uri; } else if (host && user && password && database) { body.uri = `postgresql://${user}:${password}@${host}:${port}/${database}`; } const r = await fetch(`${API_BASE}/api/postgres/get-tables`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); return r.json(); } // Get PostgreSQL table counts async function getPostgresTableCounts(uri, schema = '') { const r = await fetch(`${API_BASE}/api/postgres/get-table-counts`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); return r.json(); } // Parse PostgreSQL URI async function parsePostgresUri(uri) { const r = await fetch(`${API_BASE}/api/postgres/parse-uri`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uri }) }); return r.json(); } // Start PostgreSQL to PostgreSQL migration async function startPostgresMigration(sourceUri, destUri, schemas = null, tables = null) { const r = await fetch(`${API_BASE}/api/postgres/start-migration`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ source_uri: sourceUri, dest_uri: destUri, schemas, tables }) }); return r.json(); } // Get PostgreSQL migration status async function getPostgresMigrationStatus(migrationId) { const r = await fetch(`${API_BASE}/api/postgres/migration-status/${migrationId}`); return r.json(); } // List PostgreSQL migrations async function listPostgresMigrations() { const r = await fetch(`${API_BASE}/api/postgres/list-migrations`); return r.json(); } // ============================================================================ // S3 API Functions // ============================================================================ // Test source S3 connection async function testSourceS3Connection(options = {}) { const { useEnvVars = false, accessKeyId, secretAccessKey, region = 'us-east-1', endpointUrl, sessionToken } = options; const body = { use_env_vars: useEnvVars, access_key_id: accessKeyId, secret_access_key: secretAccessKey, region, endpoint_url: endpointUrl, session_token: sessionToken }; const r = await fetch(`${API_BASE}/api/s3-source/test-connection`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); return r.json(); } // Test destination S3 connection async function testDestinationS3Connection(options = {}) { const { useEnvVars = false, accessKeyId, secretAccessKey, region = 'us-east-1', endpointUrl, sessionToken } = options; const body = { use_env_vars: useEnvVars, access_key_id: accessKeyId, secret_access_key: secretAccessKey, region, endpoint_url: endpointUrl, session_token: sessionToken }; const r = await fetch(`${API_BASE}/api/s3-destination/test-connection`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); return r.json(); } // List source S3 buckets async function listSourceS3Buckets(accessKeyId, secretAccessKey, region = 'us-east-1', endpointUrl = null, sessionToken = null) { const r = await fetch(`${API_BASE}/api/s3-source/list-buckets`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ access_key_id: accessKeyId, secret_access_key: secretAccessKey, region, endpoint_url: endpointUrl, session_token: sessionToken }) }); return r.json(); } // List destination S3 buckets async function listDestinationS3Buckets(accessKeyId, secretAccessKey, region = 'us-east-1', endpointUrl = null, sessionToken = null) { const r = await fetch(`${API_BASE}/api/s3-destination/list-buckets`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ access_key_id: accessKeyId, secret_access_key: secretAccessKey, region, endpoint_url: endpointUrl, session_token: sessionToken }) }); return r.json(); } // List objects in S3 bucket async function listS3Objects(bucket, prefix = '', isSource = true, credentials = {}) { const body = { bucket, prefix, is_source: isSource, access_key_id: credentials.accessKeyId, secret_access_key: credentials.secretAccessKey, region: credentials.region || 'us-east-1', endpoint_url: credentials.endpointUrl, session_token: credentials.sessionToken, max_keys: credentials.maxKeys || 1000 }; const r = await fetch(`${API_BASE}/api/s3/list-objects`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); return r.json(); } // Create S3 bucket in destination async function createS3Bucket(bucket, region = 'us-east-1', options = {}) { const body = { bucket, region, access_key_id: options.accessKeyId, secret_access_key: options.secretAccessKey, endpoint_url: options.endpointUrl, session_token: options.sessionToken }; const r = await fetch(`${API_BASE}/api/s3-destination/create-bucket`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); return r.json(); } // Migrate single S3 object async function migrateS3Object(sourceBucket, sourceKey, destBucket, destKey = null, options = {}) { const body = { source_bucket: sourceBucket, source_key: sourceKey, dest_bucket: destBucket, dest_key: destKey || sourceKey, source_access_key_id: options.sourceAccessKeyId, source_secret_access_key: options.sourceSecretAccessKey, source_region: options.sourceRegion || 'us-east-1', source_endpoint_url: options.sourceEndpointUrl, source_session_token: options.sourceSessionToken, dest_access_key_id: options.destAccessKeyId, dest_secret_access_key: options.destSecretAccessKey, dest_region: options.destRegion || 'us-east-1', dest_endpoint_url: options.destEndpointUrl, dest_session_token: options.destSessionToken, preserve_metadata: options.preserveMetadata !== undefined ? options.preserveMetadata : true, storage_class: options.storageClass || 'STANDARD' }; const r = await fetch(`${API_BASE}/api/s3/migrate-object`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); return r.json(); } // Migrate multiple S3 objects in batch async function migrateS3Batch(objects, sourceBucket, destBucket, options = {}) { const body = { objects, source_bucket: sourceBucket, dest_bucket: destBucket, source_access_key_id: options.sourceAccessKeyId, source_secret_access_key: options.sourceSecretAccessKey, source_region: options.sourceRegion || 'us-east-1', source_endpoint_url: options.sourceEndpointUrl, source_session_token: options.sourceSessionToken, dest_access_key_id: options.destAccessKeyId, dest_secret_access_key: options.destSecretAccessKey, dest_region: options.destRegion || 'us-east-1', dest_endpoint_url: options.destEndpointUrl, dest_session_token: options.destSessionToken, preserve_metadata: options.preserveMetadata !== undefined ? options.preserveMetadata : true, storage_class: options.storageClass || 'STANDARD', max_concurrent: options.maxConcurrent || 5 }; const r = await fetch(`${API_BASE}/api/s3/migrate-batch`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); return r.json(); } // Start full S3 to S3 migration async function startS3Migration(sourceBucket, destBucket, prefix = '', options = {}) { const body = { source_bucket: sourceBucket, dest_bucket: destBucket, prefix, source_access_key_id: options.sourceAccessKeyId, source_secret_access_key: options.sourceSecretAccessKey, source_region: options.sourceRegion || 'us-east-1', source_endpoint_url: options.sourceEndpointUrl, source_session_token: options.sourceSessionToken, dest_access_key_id: options.destAccessKeyId, dest_secret_access_key: options.destSecretAccessKey, dest_region: options.destRegion || 'us-east-1', dest_endpoint_url: options.destEndpointUrl, dest_session_token: options.destSessionToken, include_patterns: options.includePatterns, exclude_patterns: options.excludePatterns, preserve_metadata: options.preserveMetadata !== undefined ? options.preserveMetadata : true, storage_class: options.storageClass || 'STANDARD', create_dest_bucket: options.createDestBucket !== undefined ? options.createDestBucket : true, max_concurrent: options.maxConcurrent || 5 }; const r = await fetch(`${API_BASE}/api/s3/start-migration`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); return r.json(); } // Get S3 migration status async function getS3MigrationStatus(migrationId) { const r = await fetch(`${API_BASE}/api/s3/migration-status/${migrationId}`); return r.json(); } // List S3 migrations async function listS3Migrations() { const r = await fetch(`${API_BASE}/api/s3/list-migrations`); return r.json(); } // Cancel S3 migration async function cancelS3Migration(migrationId) { const r = await fetch(`${API_BASE}/api/s3/cancel-migration/${migrationId}`, { method: "POST", headers: { "Content-Type": "application/json" } }); return r.json(); } // Parse S3 URI async function parseS3Uri(s3Uri) { const r = await fetch(`${API_BASE}/api/s3/parse-uri`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ s3_uri: s3Uri }) }); return r.json(); } // Generate presigned URL for S3 object async function generatePresignedUrl(bucket, key, isSource = true, expiration = 3600, credentials = {}) { const body = { bucket, key, is_source: isSource, expiration, access_key_id: credentials.accessKeyId, secret_access_key: credentials.secretAccessKey, region: credentials.region || 'us-east-1', endpoint_url: credentials.endpointUrl, session_token: credentials.sessionToken }; const r = await fetch(`${API_BASE}/api/s3/generate-presigned-url`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); return r.json(); } // ============================================================================ // PostgreSQL to S3 API Functions // ============================================================================ // Test PostgreSQL connection for PG to S3 async function testPgToS3PostgresConnection(uri) { const r = await fetch(`${API_BASE}/api/postgres-s3/test-postgres-connection`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uri }) }); return r.json(); } // Test S3 connection for PG to S3 async function testPgToS3S3Connection(accessKeyId, secretAccessKey, region = 'us-east-1', endpointUrl = null) { const r = await fetch(`${API_BASE}/api/postgres-s3/test-s3-connection`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ access_key_id: accessKeyId, secret_access_key: secretAccessKey, region, endpoint_url: endpointUrl }) }); return r.json(); } // Get PostgreSQL schemas for PG to S3 async function getPgToS3Schemas(uri) { const r = await fetch(`${API_BASE}/api/postgres-s3/get-schemas`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uri }) }); return r.json(); } // Get PostgreSQL tables for PG to S3 async function getPgToS3Tables(uri, schema = '') { const r = await fetch(`${API_BASE}/api/postgres-s3/get-tables`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uri, schema }) }); return r.json(); } // Export single PostgreSQL table to S3 async function exportTableToS3(postgresUri, schema, table, s3Bucket, s3Key, options = {}) { const body = { postgres_uri: postgresUri, schema, table, s3_bucket: s3Bucket, s3_key: s3Key, compress: options.compress !== undefined ? options.compress : true, format: options.format || 'csv', access_key_id: options.accessKeyId, secret_access_key: options.secretAccessKey, region: options.region || 'us-east-1', endpoint_url: options.endpointUrl }; const r = await fetch(`${API_BASE}/api/postgres-s3/export-table`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); return r.json(); } // Start full PostgreSQL to S3 migration async function startPgToS3Migration(postgresUri, s3Bucket, s3Prefix = '', options = {}) { const body = { postgres_uri: postgresUri, s3_bucket: s3Bucket, s3_prefix: s3Prefix, schemas: options.schemas, tables: options.tables, compress: options.compress !== undefined ? options.compress : true, format: options.format || 'csv', access_key_id: options.accessKeyId, secret_access_key: options.secretAccessKey, region: options.region || 'us-east-1', endpoint_url: options.endpointUrl }; const r = await fetch(`${API_BASE}/api/postgres-s3/start-migration`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); return r.json(); } // Get PostgreSQL to S3 migration status async function getPgToS3MigrationStatus(migrationId) { const r = await fetch(`${API_BASE}/api/postgres-s3/migration-status/${migrationId}`); return r.json(); } // List PostgreSQL to S3 migrations async function listPgToS3Migrations() { const r = await fetch(`${API_BASE}/api/postgres-s3/list-migrations`); return r.json(); } // ============================================================================ // Common Environment Functions // ============================================================================ // Inject environment variables async function injectEnv(envVars) { const r = await fetch(`${API_BASE}/api/inject-env`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ environment_variables: envVars }) }); return r.json(); } // Get current environment async function getCurrentEnv() { const r = await fetch(`${API_BASE}/api/get-current-env`); return r.json(); } // Health check async function healthCheck() { const r = await fetch(`${API_BASE}/api/health`); return r.json(); } // ============================================================================ // Security API Functions // ============================================================================ // Clear session async function clearSession() { const r = await fetch(`${API_BASE}/api/clear-session`, { method: "POST", headers: { "Content-Type": "application/json" } }); return r.json(); } // Clear migration async function clearMigration(migrationId) { const r = await fetch(`${API_BASE}/api/clear-migration/${migrationId}`, { method: "POST", headers: { "Content-Type": "application/json" } }); return r.json(); } // Get security status async function getSecurityStatus() { const r = await fetch(`${API_BASE}/api/security-status`); return r.json(); } // ============================================================================ // Main App // ============================================================================ function App() { const root = document.getElementById("root"); root.innerHTML = `

🔄 Universal Migrator

PostgreSQL & S3 Migration Tool

🔒 Secure Mode

🐘 → 📤 PostgreSQL to S3 Migration

PostgreSQL Source

S3 Destination

🐘 → 🐘 PostgreSQL to PostgreSQL Migration

Source PostgreSQL

Destination PostgreSQL

Migration Options

📤 → 📥 S3 to S3 Migration

Source S3

Destination S3

⚙️ Environment & Security

Environment Variables

Current Configuration

Environment Preview


            

Security

Session ID: Loading...

Active Sessions: -

Expiry: 10 minutes

Encryption: AES-256
Session Isolation: ✅ Enabled
Auto-cleanup: 10 minutes

Migration Data Cleanup

Clear sensitive data from migrations

Migration History

Ready
`; // ==================== State Variables ==================== // ===== PSQL to S3 ===== let psqlToS3Config = { source: { host: '', user: '', password: '', port: 5432, database: '', uri: '' }, s3: { accessKeyId: '', secretAccessKey: '', region: 'us-east-1', endpointUrl: '', bucket: '', prefix: '' }, compress: true, format: 'csv' }; // ===== PSQL to PSQL ===== let psqlToPsqlConfig = { source: { host: '', user: '', password: '', port: 5432, database: '', uri: '' }, dest: { host: '', user: '', password: '', port: 5432, database: '', uri: '' } }; let psqlToPsqlSchemas = []; let psqlToPsqlTables = []; let psqlToPsqlSelectedSchemas = []; let psqlToPsqlSelectedTables = []; // ===== S3 to S3 ===== let s3ToS3Config = { source: { accessKeyId: '', secretAccessKey: '', region: 'us-east-1', endpointUrl: '', sessionToken: '', bucket: 'my-source-bucket' }, dest: { accessKeyId: '', secretAccessKey: '', region: 'us-east-1', endpointUrl: '', sessionToken: '', bucket: 'my-destination-bucket' } }; let s3ToS3SourceBuckets = []; let s3ToS3DestBuckets = []; // Common state variables let migrations = []; let activeMigration = null; let migrationLogs = []; let currentEnv = {}; let loading = false; // DOM Elements const statusInfo = document.getElementById("statusInfo"); const loadingDiv = document.getElementById("loading"); const envPreview = document.getElementById("envPreview"); const sourcePostgresSummary = document.getElementById("sourcePostgresSummary"); const destPostgresSummary = document.getElementById("destPostgresSummary"); const sourceS3Summary = document.getElementById("sourceS3Summary"); const destS3Summary = document.getElementById("destS3Summary"); // ==================== Setup Event Listeners ==================== function setupEventListeners() { // Tab switching document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); btn.classList.add('active'); document.getElementById(`tab-${btn.dataset.tab}`).classList.add('active'); }); }); // ========== PSQL to S3 Inputs ========== document.getElementById('psqlToS3_sourceHost')?.addEventListener('input', (e) => { psqlToS3Config.source.host = e.target.value; }); document.getElementById('psqlToS3_sourcePort')?.addEventListener('input', (e) => { psqlToS3Config.source.port = parseInt(e.target.value) || 5432; }); document.getElementById('psqlToS3_sourceUser')?.addEventListener('input', (e) => { psqlToS3Config.source.user = e.target.value; }); document.getElementById('psqlToS3_sourcePassword')?.addEventListener('input', (e) => { psqlToS3Config.source.password = e.target.value; }); document.getElementById('psqlToS3_sourceDatabase')?.addEventListener('input', (e) => { psqlToS3Config.source.database = e.target.value; }); document.getElementById('psqlToS3_sourceUri')?.addEventListener('input', (e) => { psqlToS3Config.source.uri = e.target.value; }); document.getElementById('psqlToS3_s3AccessKeyId')?.addEventListener('input', (e) => { psqlToS3Config.s3.accessKeyId = e.target.value; }); document.getElementById('psqlToS3_s3SecretAccessKey')?.addEventListener('input', (e) => { psqlToS3Config.s3.secretAccessKey = e.target.value; }); document.getElementById('psqlToS3_s3Region')?.addEventListener('input', (e) => { psqlToS3Config.s3.region = e.target.value || 'us-east-1'; }); document.getElementById('psqlToS3_s3EndpointUrl')?.addEventListener('input', (e) => { psqlToS3Config.s3.endpointUrl = e.target.value; }); document.getElementById('psqlToS3_s3Bucket')?.addEventListener('input', (e) => { psqlToS3Config.s3.bucket = e.target.value; }); document.getElementById('psqlToS3_s3Prefix')?.addEventListener('input', (e) => { psqlToS3Config.s3.prefix = e.target.value; }); document.getElementById('psqlToS3_compress')?.addEventListener('change', (e) => { psqlToS3Config.compress = e.target.checked; }); document.getElementById('psqlToS3_format')?.addEventListener('change', (e) => { psqlToS3Config.format = e.target.value; }); // ========== PSQL to PSQL Inputs ========== document.getElementById('psqlToPsql_sourceHost')?.addEventListener('input', (e) => { psqlToPsqlConfig.source.host = e.target.value; }); document.getElementById('psqlToPsql_sourcePort')?.addEventListener('input', (e) => { psqlToPsqlConfig.source.port = parseInt(e.target.value) || 5432; }); document.getElementById('psqlToPsql_sourceUser')?.addEventListener('input', (e) => { psqlToPsqlConfig.source.user = e.target.value; }); document.getElementById('psqlToPsql_sourcePassword')?.addEventListener('input', (e) => { psqlToPsqlConfig.source.password = e.target.value; }); document.getElementById('psqlToPsql_sourceDatabase')?.addEventListener('input', (e) => { psqlToPsqlConfig.source.database = e.target.value; }); document.getElementById('psqlToPsql_sourceUri')?.addEventListener('input', (e) => { psqlToPsqlConfig.source.uri = e.target.value; }); document.getElementById('psqlToPsql_destHost')?.addEventListener('input', (e) => { psqlToPsqlConfig.dest.host = e.target.value; }); document.getElementById('psqlToPsql_destPort')?.addEventListener('input', (e) => { psqlToPsqlConfig.dest.port = parseInt(e.target.value) || 5432; }); document.getElementById('psqlToPsql_destUser')?.addEventListener('input', (e) => { psqlToPsqlConfig.dest.user = e.target.value; }); document.getElementById('psqlToPsql_destPassword')?.addEventListener('input', (e) => { psqlToPsqlConfig.dest.password = e.target.value; }); document.getElementById('psqlToPsql_destDatabase')?.addEventListener('input', (e) => { psqlToPsqlConfig.dest.database = e.target.value; }); document.getElementById('psqlToPsql_destUri')?.addEventListener('input', (e) => { psqlToPsqlConfig.dest.uri = e.target.value; }); // ========== S3 to S3 Inputs ========== document.getElementById('s3ToS3_sourceAccessKeyId')?.addEventListener('input', (e) => { s3ToS3Config.source.accessKeyId = e.target.value; }); document.getElementById('s3ToS3_sourceSecretAccessKey')?.addEventListener('input', (e) => { s3ToS3Config.source.secretAccessKey = e.target.value; }); document.getElementById('s3ToS3_sourceRegion')?.addEventListener('input', (e) => { s3ToS3Config.source.region = e.target.value || 'us-east-1'; }); document.getElementById('s3ToS3_sourceEndpointUrl')?.addEventListener('input', (e) => { s3ToS3Config.source.endpointUrl = e.target.value; }); document.getElementById('s3ToS3_sourceSessionToken')?.addEventListener('input', (e) => { s3ToS3Config.source.sessionToken = e.target.value; }); document.getElementById('s3ToS3_sourceBucket')?.addEventListener('input', (e) => { s3ToS3Config.source.bucket = e.target.value; }); document.getElementById('s3ToS3_destAccessKeyId')?.addEventListener('input', (e) => { s3ToS3Config.dest.accessKeyId = e.target.value; }); document.getElementById('s3ToS3_destSecretAccessKey')?.addEventListener('input', (e) => { s3ToS3Config.dest.secretAccessKey = e.target.value; }); document.getElementById('s3ToS3_destRegion')?.addEventListener('input', (e) => { s3ToS3Config.dest.region = e.target.value || 'us-east-1'; }); document.getElementById('s3ToS3_destEndpointUrl')?.addEventListener('input', (e) => { s3ToS3Config.dest.endpointUrl = e.target.value; }); document.getElementById('s3ToS3_destSessionToken')?.addEventListener('input', (e) => { s3ToS3Config.dest.sessionToken = e.target.value; }); document.getElementById('s3ToS3_destBucket')?.addEventListener('input', (e) => { s3ToS3Config.dest.bucket = e.target.value; }); // ========== PSQL to S3 Actions ========== document.getElementById('psqlToS3_testSourceConnection')?.addEventListener('click', testPsqlToS3SourceConnectionHandler); document.getElementById('psqlToS3_parseSourceUri')?.addEventListener('click', parsePsqlToS3SourceUriHandler); document.getElementById('psqlToS3_getSchemas')?.addEventListener('click', getPsqlToS3SchemasHandler); document.getElementById('psqlToS3_getTables')?.addEventListener('click', getPsqlToS3TablesHandler); document.getElementById('psqlToS3_testPgConnection')?.addEventListener('click', testPsqlToS3PgConnectionHandler); document.getElementById('psqlToS3_testS3Connection')?.addEventListener('click', testPsqlToS3S3ConnectionHandler); document.getElementById('psqlToS3_startMigration')?.addEventListener('click', startPsqlToS3MigrationHandler); // ========== PSQL to PSQL Actions ========== document.getElementById('psqlToPsql_testSourceConnection')?.addEventListener('click', testPsqlToPsqlSourceConnectionHandler); document.getElementById('psqlToPsql_parseSourceUri')?.addEventListener('click', parsePsqlToPsqlSourceUriHandler); document.getElementById('psqlToPsql_getSchemas')?.addEventListener('click', getPsqlToPsqlSchemasHandler); document.getElementById('psqlToPsql_getTables')?.addEventListener('click', getPsqlToPsqlTablesHandler); document.getElementById('psqlToPsql_testDestConnection')?.addEventListener('click', testPsqlToPsqlDestConnectionHandler); document.getElementById('psqlToPsql_parseDestUri')?.addEventListener('click', parsePsqlToPsqlDestUriHandler); document.getElementById('psqlToPsql_startMigration')?.addEventListener('click', startPsqlToPsqlMigrationHandler); // ========== S3 to S3 Actions ========== document.getElementById('s3ToS3_testSourceConnection')?.addEventListener('click', testS3ToS3SourceConnectionHandler); document.getElementById('s3ToS3_listSourceBuckets')?.addEventListener('click', listS3ToS3SourceBucketsHandler); document.getElementById('s3ToS3_hideSourceBuckets')?.addEventListener('click', () => { document.getElementById('s3ToS3_sourceBucketsList').style.display = 'none'; }); document.getElementById('s3ToS3_testDestConnection')?.addEventListener('click', testS3ToS3DestConnectionHandler); document.getElementById('s3ToS3_listDestBuckets')?.addEventListener('click', listS3ToS3DestBucketsHandler); document.getElementById('s3ToS3_createDestBucket')?.addEventListener('click', createS3ToS3DestBucketHandler); document.getElementById('s3ToS3_hideDestBuckets')?.addEventListener('click', () => { document.getElementById('s3ToS3_destBucketsList').style.display = 'none'; }); document.getElementById('s3ToS3_startMigration')?.addEventListener('click', startS3ToS3MigrationHandler); // ========== Environment Actions ========== document.getElementById('refreshEnv')?.addEventListener('click', loadCurrentEnvHandler); document.getElementById('injectEnv')?.addEventListener('click', injectEnvironmentHandler); document.getElementById('clearEnv')?.addEventListener('click', clearEnvironmentHandler); document.getElementById('copyEnv')?.addEventListener('click', copyEnvToClipboardHandler); // ========== Security Actions ========== document.getElementById('clearSessionBtn')?.addEventListener('click', clearSessionHandler); document.getElementById('clearMigrationBtn')?.addEventListener('click', clearMigrationHandler); document.getElementById('refreshMigrations')?.addEventListener('click', loadMigrationsHandler); } // Helper function for summaries function updateS3Summaries() { if (sourceS3Summary) sourceS3Summary.textContent = s3ToS3Config.source.bucket || 'Not set'; if (destS3Summary) destS3Summary.textContent = s3ToS3Config.dest.bucket || 'Not set'; } // ==================== Helper Functions ==================== function setLoading(isLoading) { loading = isLoading; loadingDiv.style.display = isLoading ? 'block' : 'none'; } function updatePostgresSummaries() { if (sourcePostgresSummary) { sourcePostgresSummary.textContent = psqlToPsqlConfig.source.database ? `${psqlToPsqlConfig.source.database}@${psqlToPsqlConfig.source.host}` : 'Not set'; } if (destPostgresSummary) { destPostgresSummary.textContent = psqlToPsqlConfig.dest.database ? `${psqlToPsqlConfig.dest.database}@${psqlToPsqlConfig.dest.host}` : 'Not set'; } } function updateStatusInfo() { let info = []; if (psqlToPsqlConfig.source.database) info.push(`🐘 Source PG: ${psqlToPsqlConfig.source.database}`); if (psqlToPsqlConfig.dest.database) info.push(`🐘 Dest PG: ${psqlToPsqlConfig.dest.database}`); if (s3ToS3Config.source.bucket) info.push(`📤 S3: ${s3ToS3Config.source.bucket}`); if (s3ToS3Config.dest.bucket) info.push(`📥 S3: ${s3ToS3Config.dest.bucket}`); if (activeMigration) info.push(`🚀 Migration: ${activeMigration}`); info.push(`⚡ ${Object.keys(currentEnv).length} env vars`); statusInfo.textContent = info.join(' • ') || 'Ready'; } // ==================== PSQL to S3 Handlers ==================== async function testPsqlToS3SourceConnectionHandler() { setLoading(true); try { let uri = psqlToS3Config.source.uri; if (!uri && psqlToS3Config.source.host && psqlToS3Config.source.user && psqlToS3Config.source.password && psqlToS3Config.source.database) { uri = `postgresql://${psqlToS3Config.source.user}:${psqlToS3Config.source.password}@${psqlToS3Config.source.host}:${psqlToS3Config.source.port}/${psqlToS3Config.source.database}`; } const result = await testPostgresConnection({ uri }); const statusDiv = document.getElementById('psqlToS3_sourceConnectionStatus'); if (result.success) { showNotification(`✅ PSQL to S3 - Source connection successful!`, 'success'); statusDiv.innerHTML = `

Success: ✅ Connected

Host: ${psqlToS3Config.source.host || result.connection?.host}:${psqlToS3Config.source.port || result.connection?.port}

Version: ${result.version || 'Unknown'}

`; statusDiv.className = 'status-message success'; } else { showNotification(`❌ PSQL to S3 - Source connection failed: ${result.error}`, 'error'); statusDiv.innerHTML = `

Error: ${result.error}

`; statusDiv.className = 'status-message error'; } statusDiv.style.display = 'block'; } catch (error) { showNotification(`❌ Error testing source PostgreSQL connection: ${error.message}`, 'error'); } setLoading(false); } async function parsePsqlToS3SourceUriHandler() { const uri = document.getElementById('psqlToS3_sourceUri')?.value; if (!uri) { showNotification('Please enter PostgreSQL URI', 'warning'); return; } setLoading(true); try { const result = await parsePostgresUri(uri); if (result.success && result.parsed) { psqlToS3Config.source = { host: result.parsed.host || '', user: result.parsed.user || '', password: result.parsed.password || '', port: result.parsed.port || 5432, database: result.parsed.database || '', uri: uri }; document.getElementById('psqlToS3_sourceHost').value = result.parsed.host; document.getElementById('psqlToS3_sourceUser').value = result.parsed.user; document.getElementById('psqlToS3_sourcePassword').value = result.parsed.password; document.getElementById('psqlToS3_sourcePort').value = result.parsed.port; document.getElementById('psqlToS3_sourceDatabase').value = result.parsed.database; showNotification('✅ Source PostgreSQL URI parsed successfully', 'success'); } else { showNotification(`❌ Failed to parse PostgreSQL URI: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error parsing PostgreSQL URI: ${error.message}`, 'error'); } setLoading(false); } async function getPsqlToS3SchemasHandler() { setLoading(true); try { const params = {}; if (psqlToS3Config.source.uri) { params.uri = psqlToS3Config.source.uri; } else if (psqlToS3Config.source.host && psqlToS3Config.source.user && psqlToS3Config.source.password && psqlToS3Config.source.database) { params.host = psqlToS3Config.source.host; params.user = psqlToS3Config.source.user; params.password = psqlToS3Config.source.password; params.database = psqlToS3Config.source.database; params.port = psqlToS3Config.source.port; } else { showNotification('Please enter source PostgreSQL connection details', 'warning'); setLoading(false); return; } const result = await getPostgresSchemas(params); if (result.success) { renderPsqlToS3Schemas(result.schemas || []); document.getElementById('psqlToS3_schemasSection').style.display = 'block'; showNotification(`✅ Found ${result.count} schema(s)`, 'success'); } else { showNotification(`❌ Failed to get schemas: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error getting schemas: ${error.message}`, 'error'); } setLoading(false); } async function getPsqlToS3TablesHandler() { setLoading(true); try { const params = {}; if (psqlToS3Config.source.uri) { params.uri = psqlToS3Config.source.uri; } else if (psqlToS3Config.source.host && psqlToS3Config.source.user && psqlToS3Config.source.password && psqlToS3Config.source.database) { params.host = psqlToS3Config.source.host; params.user = psqlToS3Config.source.user; params.password = psqlToS3Config.source.password; params.database = psqlToS3Config.source.database; params.port = psqlToS3Config.source.port; } else { showNotification('Please enter source PostgreSQL connection details', 'warning'); setLoading(false); return; } const result = await getPostgresTables(params); if (result.success) { renderPsqlToS3Tables(result.tables || []); document.getElementById('psqlToS3_tablesSection').style.display = 'block'; showNotification(`✅ Found ${result.count} table(s)`, 'success'); } else { showNotification(`❌ Failed to get tables: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error getting tables: ${error.message}`, 'error'); } setLoading(false); } async function testPsqlToS3PgConnectionHandler() { if (!psqlToS3Config.source.uri && !psqlToS3Config.source.host) { showNotification('Please enter PostgreSQL connection details', 'warning'); return; } setLoading(true); try { const uri = psqlToS3Config.source.uri || buildPostgresConnectionString( psqlToS3Config.source.host, psqlToS3Config.source.database, psqlToS3Config.source.user, psqlToS3Config.source.port ); const result = await testPgToS3PostgresConnection(uri); if (result.success) { showNotification(`✅ PostgreSQL connection successful!`, 'success'); } else { showNotification(`❌ PostgreSQL connection failed: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error testing PostgreSQL connection: ${error.message}`, 'error'); } setLoading(false); } async function testPsqlToS3S3ConnectionHandler() { setLoading(true); try { const result = await testPgToS3S3Connection( psqlToS3Config.s3.accessKeyId, psqlToS3Config.s3.secretAccessKey, psqlToS3Config.s3.region, psqlToS3Config.s3.endpointUrl ); if (result.success) { showNotification(`✅ S3 connection successful!`, 'success'); } else { showNotification(`❌ S3 connection failed: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error testing S3 connection: ${error.message}`, 'error'); } setLoading(false); } async function startPsqlToS3MigrationHandler() { if (!psqlToS3Config.source.uri && !psqlToS3Config.source.host) { showNotification('Please enter PostgreSQL connection details', 'warning'); return; } if (!psqlToS3Config.s3.bucket) { showNotification('Please enter S3 bucket name', 'warning'); return; } setLoading(true); try { const uri = psqlToS3Config.source.uri || buildPostgresConnectionString( psqlToS3Config.source.host, psqlToS3Config.source.database, psqlToS3Config.source.user, psqlToS3Config.source.port ); const result = await startPgToS3Migration( uri, psqlToS3Config.s3.bucket, psqlToS3Config.s3.prefix, { compress: psqlToS3Config.compress, format: psqlToS3Config.format, accessKeyId: psqlToS3Config.s3.accessKeyId, secretAccessKey: psqlToS3Config.s3.secretAccessKey, region: psqlToS3Config.s3.region, endpointUrl: psqlToS3Config.s3.endpointUrl } ); if (result.success) { activeMigration = result.migration_id; showNotification(`✅ PostgreSQL to S3 migration ${activeMigration} started!`, 'success'); pollPgToS3MigrationStatus(activeMigration); } else { showNotification(`❌ Failed to start migration: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error starting migration: ${error.message}`, 'error'); } setLoading(false); } // ==================== PSQL to PSQL Handlers ==================== async function testPsqlToPsqlSourceConnectionHandler() { setLoading(true); try { let uri = psqlToPsqlConfig.source.uri; if (!uri && psqlToPsqlConfig.source.host && psqlToPsqlConfig.source.user && psqlToPsqlConfig.source.password && psqlToPsqlConfig.source.database) { uri = `postgresql://${psqlToPsqlConfig.source.user}:${psqlToPsqlConfig.source.password}@${psqlToPsqlConfig.source.host}:${psqlToPsqlConfig.source.port}/${psqlToPsqlConfig.source.database}`; } const result = await testPostgresConnection({ uri }); const statusDiv = document.getElementById('psqlToPsql_sourceConnectionStatus'); if (result.success) { showNotification(`✅ PSQL to PSQL - Source connection successful!`, 'success'); statusDiv.innerHTML = `

Success: ✅ Connected

Host: ${psqlToPsqlConfig.source.host || result.connection?.host}:${psqlToPsqlConfig.source.port || result.connection?.port}

Version: ${result.version || 'Unknown'}

`; statusDiv.className = 'status-message success'; } else { showNotification(`❌ PSQL to PSQL - Source connection failed: ${result.error}`, 'error'); statusDiv.innerHTML = `

Error: ${result.error}

`; statusDiv.className = 'status-message error'; } statusDiv.style.display = 'block'; } catch (error) { showNotification(`❌ Error testing source PostgreSQL connection: ${error.message}`, 'error'); } setLoading(false); } async function parsePsqlToPsqlSourceUriHandler() { const uri = document.getElementById('psqlToPsql_sourceUri')?.value; if (!uri) { showNotification('Please enter PostgreSQL URI', 'warning'); return; } setLoading(true); try { const result = await parsePostgresUri(uri); if (result.success && result.parsed) { psqlToPsqlConfig.source = { host: result.parsed.host || '', user: result.parsed.user || '', password: result.parsed.password || '', port: result.parsed.port || 5432, database: result.parsed.database || '', uri: uri }; document.getElementById('psqlToPsql_sourceHost').value = result.parsed.host; document.getElementById('psqlToPsql_sourceUser').value = result.parsed.user; document.getElementById('psqlToPsql_sourcePassword').value = result.parsed.password; document.getElementById('psqlToPsql_sourcePort').value = result.parsed.port; document.getElementById('psqlToPsql_sourceDatabase').value = result.parsed.database; showNotification('✅ Source PostgreSQL URI parsed successfully', 'success'); } else { showNotification(`❌ Failed to parse PostgreSQL URI: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error parsing PostgreSQL URI: ${error.message}`, 'error'); } setLoading(false); } async function getPsqlToPsqlSchemasHandler() { setLoading(true); try { const params = {}; if (psqlToPsqlConfig.source.uri) { params.uri = psqlToPsqlConfig.source.uri; } else if (psqlToPsqlConfig.source.host && psqlToPsqlConfig.source.user && psqlToPsqlConfig.source.password && psqlToPsqlConfig.source.database) { params.host = psqlToPsqlConfig.source.host; params.user = psqlToPsqlConfig.source.user; params.password = psqlToPsqlConfig.source.password; params.database = psqlToPsqlConfig.source.database; params.port = psqlToPsqlConfig.source.port; } else { showNotification('Please enter source PostgreSQL connection details', 'warning'); setLoading(false); return; } const result = await getPostgresSchemas(params); if (result.success) { psqlToPsqlSchemas = result.schemas || []; renderPsqlToPsqlSchemas(); document.getElementById('psqlToPsql_schemasSection').style.display = 'block'; showNotification(`✅ Found ${result.count} schema(s)`, 'success'); } else { showNotification(`❌ Failed to get schemas: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error getting schemas: ${error.message}`, 'error'); } setLoading(false); } async function getPsqlToPsqlTablesHandler() { setLoading(true); try { const params = {}; if (psqlToPsqlConfig.source.uri) { params.uri = psqlToPsqlConfig.source.uri; } else if (psqlToPsqlConfig.source.host && psqlToPsqlConfig.source.user && psqlToPsqlConfig.source.password && psqlToPsqlConfig.source.database) { params.host = psqlToPsqlConfig.source.host; params.user = psqlToPsqlConfig.source.user; params.password = psqlToPsqlConfig.source.password; params.database = psqlToPsqlConfig.source.database; params.port = psqlToPsqlConfig.source.port; } else { showNotification('Please enter source PostgreSQL connection details', 'warning'); setLoading(false); return; } const result = await getPostgresTables(params); if (result.success) { psqlToPsqlTables = result.tables || []; renderPsqlToPsqlTables(); document.getElementById('psqlToPsql_tablesSection').style.display = 'block'; showNotification(`✅ Found ${result.count} table(s)`, 'success'); } else { showNotification(`❌ Failed to get tables: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error getting tables: ${error.message}`, 'error'); } setLoading(false); } async function testPsqlToPsqlDestConnectionHandler() { setLoading(true); try { let uri = psqlToPsqlConfig.dest.uri; if (!uri && psqlToPsqlConfig.dest.host && psqlToPsqlConfig.dest.user && psqlToPsqlConfig.dest.password && psqlToPsqlConfig.dest.database) { uri = `postgresql://${psqlToPsqlConfig.dest.user}:${psqlToPsqlConfig.dest.password}@${psqlToPsqlConfig.dest.host}:${psqlToPsqlConfig.dest.port}/${psqlToPsqlConfig.dest.database}`; } const result = await testPostgresConnection({ uri }); const statusDiv = document.getElementById('psqlToPsql_destConnectionStatus'); if (result.success) { showNotification(`✅ PSQL to PSQL - Destination connection successful!`, 'success'); statusDiv.innerHTML = `

Success: ✅ Connected

Host: ${psqlToPsqlConfig.dest.host || result.connection?.host}:${psqlToPsqlConfig.dest.port || result.connection?.port}

Version: ${result.version || 'Unknown'}

`; statusDiv.className = 'status-message success'; } else { showNotification(`❌ PSQL to PSQL - Destination connection failed: ${result.error}`, 'error'); statusDiv.innerHTML = `

Error: ${result.error}

`; statusDiv.className = 'status-message error'; } statusDiv.style.display = 'block'; } catch (error) { showNotification(`❌ Error testing destination PostgreSQL connection: ${error.message}`, 'error'); } setLoading(false); } async function parsePsqlToPsqlDestUriHandler() { const uri = document.getElementById('psqlToPsql_destUri')?.value; if (!uri) { showNotification('Please enter destination PostgreSQL URI', 'warning'); return; } setLoading(true); try { const result = await parsePostgresUri(uri); if (result.success && result.parsed) { psqlToPsqlConfig.dest = { host: result.parsed.host || '', user: result.parsed.user || '', password: result.parsed.password || '', port: result.parsed.port || 5432, database: result.parsed.database || '', uri: uri }; document.getElementById('psqlToPsql_destHost').value = result.parsed.host; document.getElementById('psqlToPsql_destUser').value = result.parsed.user; document.getElementById('psqlToPsql_destPassword').value = result.parsed.password; document.getElementById('psqlToPsql_destPort').value = result.parsed.port; document.getElementById('psqlToPsql_destDatabase').value = result.parsed.database; showNotification('✅ Destination PostgreSQL URI parsed successfully', 'success'); updatePostgresSummaries(); } else { showNotification(`❌ Failed to parse PostgreSQL URI: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error parsing PostgreSQL URI: ${error.message}`, 'error'); } setLoading(false); } async function startPsqlToPsqlMigrationHandler() { let sourceUri = psqlToPsqlConfig.source.uri || buildPostgresConnectionString( psqlToPsqlConfig.source.host, psqlToPsqlConfig.source.database, psqlToPsqlConfig.source.user, psqlToPsqlConfig.source.port ); let destUri = psqlToPsqlConfig.dest.uri || buildPostgresConnectionString( psqlToPsqlConfig.dest.host, psqlToPsqlConfig.dest.database, psqlToPsqlConfig.dest.user, psqlToPsqlConfig.dest.port ); if (!sourceUri) { showNotification('Please enter source PostgreSQL connection details', 'warning'); return; } if (!destUri) { showNotification('Please enter destination PostgreSQL connection details', 'warning'); return; } setLoading(true); try { const result = await startPostgresMigration( sourceUri, destUri, psqlToPsqlSelectedSchemas.length > 0 ? psqlToPsqlSelectedSchemas : null, psqlToPsqlSelectedTables.length > 0 ? psqlToPsqlSelectedTables : null ); if (result.success) { activeMigration = result.migration_id; showNotification(`✅ PostgreSQL migration ${activeMigration} started!`, 'success'); pollPostgresMigrationStatus(activeMigration); } else { showNotification(`❌ Failed to start migration: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error starting migration: ${error.message}`, 'error'); } setLoading(false); } // ==================== S3 to S3 Handlers ==================== async function testS3ToS3SourceConnectionHandler() { setLoading(true); try { const result = await testSourceS3Connection({ useEnvVars: false, accessKeyId: s3ToS3Config.source.accessKeyId, secretAccessKey: s3ToS3Config.source.secretAccessKey, region: s3ToS3Config.source.region, endpointUrl: s3ToS3Config.source.endpointUrl, sessionToken: s3ToS3Config.source.sessionToken }); const statusDiv = document.getElementById('s3ToS3_sourceConnectionStatus'); if (result.success) { showNotification(`✅ S3 to S3 - Source connection successful!`, 'success'); statusDiv.innerHTML = `

Success: ✅ Connected

Endpoint: ${s3ToS3Config.source.endpointUrl || 'AWS S3 (default)'}

Region: ${s3ToS3Config.source.region}

Buckets Found: ${result.bucket_count || 0}

`; statusDiv.className = 'status-message success'; } else { showNotification(`❌ S3 to S3 - Source connection failed: ${result.error}`, 'error'); statusDiv.innerHTML = `

Error: ${result.error}

`; statusDiv.className = 'status-message error'; } statusDiv.style.display = 'block'; } catch (error) { showNotification(`❌ Error testing source S3 connection: ${error.message}`, 'error'); } setLoading(false); } async function listS3ToS3SourceBucketsHandler() { setLoading(true); try { const result = await listSourceS3Buckets( s3ToS3Config.source.accessKeyId, s3ToS3Config.source.secretAccessKey, s3ToS3Config.source.region, s3ToS3Config.source.endpointUrl, s3ToS3Config.source.sessionToken ); if (result.success) { s3ToS3SourceBuckets = result.buckets || []; renderS3ToS3SourceBuckets(); document.getElementById('s3ToS3_sourceBucketsList').style.display = 'block'; showNotification(`✅ Found ${s3ToS3SourceBuckets.length} source bucket(s)`, 'success'); } else { showNotification(`❌ Failed to list source buckets: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error listing source buckets: ${error.message}`, 'error'); } setLoading(false); } async function testS3ToS3DestConnectionHandler() { setLoading(true); try { const result = await testDestinationS3Connection({ useEnvVars: false, accessKeyId: s3ToS3Config.dest.accessKeyId, secretAccessKey: s3ToS3Config.dest.secretAccessKey, region: s3ToS3Config.dest.region, endpointUrl: s3ToS3Config.dest.endpointUrl, sessionToken: s3ToS3Config.dest.sessionToken }); const statusDiv = document.getElementById('s3ToS3_destConnectionStatus'); if (result.success) { showNotification(`✅ S3 to S3 - Destination connection successful!`, 'success'); statusDiv.innerHTML = `

Success: ✅ Connected

Endpoint: ${s3ToS3Config.dest.endpointUrl || 'AWS S3 (default)'}

Region: ${s3ToS3Config.dest.region}

Buckets Found: ${result.bucket_count || 0}

`; statusDiv.className = 'status-message success'; } else { showNotification(`❌ S3 to S3 - Destination connection failed: ${result.error}`, 'error'); statusDiv.innerHTML = `

Error: ${result.error}

`; statusDiv.className = 'status-message error'; } statusDiv.style.display = 'block'; } catch (error) { showNotification(`❌ Error testing destination S3 connection: ${error.message}`, 'error'); } setLoading(false); } async function listS3ToS3DestBucketsHandler() { setLoading(true); try { const result = await listDestinationS3Buckets( s3ToS3Config.dest.accessKeyId, s3ToS3Config.dest.secretAccessKey, s3ToS3Config.dest.region, s3ToS3Config.dest.endpointUrl, s3ToS3Config.dest.sessionToken ); if (result.success) { s3ToS3DestBuckets = result.buckets || []; renderS3ToS3DestBuckets(); document.getElementById('s3ToS3_destBucketsList').style.display = 'block'; showNotification(`✅ Found ${s3ToS3DestBuckets.length} destination bucket(s)`, 'success'); } else { showNotification(`❌ Failed to list destination buckets: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error listing destination buckets: ${error.message}`, 'error'); } setLoading(false); } async function createS3ToS3DestBucketHandler() { if (!s3ToS3Config.dest.bucket) { showNotification('Please enter destination bucket name', 'warning'); return; } setLoading(true); try { const result = await createS3Bucket( s3ToS3Config.dest.bucket, s3ToS3Config.dest.region, { accessKeyId: s3ToS3Config.dest.accessKeyId, secretAccessKey: s3ToS3Config.dest.secretAccessKey, endpointUrl: s3ToS3Config.dest.endpointUrl, sessionToken: s3ToS3Config.dest.sessionToken } ); if (result.success) { if (result.created) { showNotification(`✅ Bucket created successfully: ${s3ToS3Config.dest.bucket}`, 'success'); } else { showNotification(`ℹ️ Bucket already exists: ${s3ToS3Config.dest.bucket}`, 'info'); } } else { showNotification(`❌ Failed to create bucket: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error creating bucket: ${error.message}`, 'error'); } setLoading(false); } async function startS3ToS3MigrationHandler() { if (!s3ToS3Config.source.bucket) { showNotification('Please select source bucket', 'warning'); return; } if (!s3ToS3Config.dest.bucket) { showNotification('Please select destination bucket', 'warning'); return; } setLoading(true); try { const includePatterns = document.getElementById('psqlToPsql_includePatterns')?.value ?.split(',').map(p => p.trim()).filter(p => p) || null; const excludePatterns = document.getElementById('psqlToPsql_excludePatterns')?.value ?.split(',').map(p => p.trim()).filter(p => p) || null; const result = await startS3Migration( s3ToS3Config.source.bucket, s3ToS3Config.dest.bucket, '', { sourceAccessKeyId: s3ToS3Config.source.accessKeyId, sourceSecretAccessKey: s3ToS3Config.source.secretAccessKey, sourceRegion: s3ToS3Config.source.region, sourceEndpointUrl: s3ToS3Config.source.endpointUrl, sourceSessionToken: s3ToS3Config.source.sessionToken, destAccessKeyId: s3ToS3Config.dest.accessKeyId, destSecretAccessKey: s3ToS3Config.dest.secretAccessKey, destRegion: s3ToS3Config.dest.region, destEndpointUrl: s3ToS3Config.dest.endpointUrl, destSessionToken: s3ToS3Config.dest.sessionToken, includePatterns, excludePatterns, preserveMetadata: document.getElementById('psqlToPsql_preserveMetadata')?.checked, storageClass: document.getElementById('psqlToPsql_storageClass')?.value, createDestBucket: document.getElementById('psqlToPsql_createDestBucket')?.checked, maxConcurrent: parseInt(document.getElementById('psqlToPsql_maxConcurrent')?.value) || 5 } ); if (result.success) { activeMigration = result.migration_id; showNotification(`✅ S3 to S3 migration ${activeMigration} started!`, 'success'); pollS3MigrationStatus(activeMigration); } else { showNotification(`❌ Failed to start migration: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error starting migration: ${error.message}`, 'error'); } setLoading(false); } // ==================== Polling Functions ==================== async function pollPostgresMigrationStatus(migrationId) { try { const status = await getPostgresMigrationStatus(migrationId); if (status.success && status.status) { if (status.status.logs) { migrationLogs = status.status.logs; renderMigrationLogs(); document.getElementById('migrationLogs').style.display = 'block'; } if (status.status.success === true) { showNotification(`✅ Migration ${migrationId} completed!`, 'success'); loadMigrationsHandler(); } else if (status.status.success === false) { showNotification(`❌ Migration ${migrationId} failed: ${status.status.error}`, 'error'); } else { setTimeout(() => pollPostgresMigrationStatus(migrationId), 2000); } } } catch (error) { console.error('Error polling migration status:', error); setTimeout(() => pollPostgresMigrationStatus(migrationId), 5000); } } async function pollS3MigrationStatus(migrationId) { try { const status = await getS3MigrationStatus(migrationId); if (status.success && status.status) { if (status.status.logs) { migrationLogs = status.status.logs; renderMigrationLogs(); document.getElementById('migrationLogs').style.display = 'block'; } if (status.status.success === true) { showNotification(`✅ Migration ${migrationId} completed!`, 'success'); loadMigrationsHandler(); } else if (status.status.success === false) { showNotification(`❌ Migration ${migrationId} failed: ${status.status.error}`, 'error'); } else { setTimeout(() => pollS3MigrationStatus(migrationId), 2000); } } } catch (error) { console.error('Error polling migration status:', error); setTimeout(() => pollS3MigrationStatus(migrationId), 5000); } } async function pollPgToS3MigrationStatus(migrationId) { try { const status = await getPgToS3MigrationStatus(migrationId); if (status.success && status.status) { if (status.status.logs) { migrationLogs = status.status.logs; renderMigrationLogs(); document.getElementById('migrationLogs').style.display = 'block'; } if (status.status.success === true) { showNotification(`✅ Migration ${migrationId} completed!`, 'success'); loadMigrationsHandler(); } else if (status.status.success === false) { showNotification(`❌ Migration ${migrationId} failed: ${status.status.error}`, 'error'); } else { setTimeout(() => pollPgToS3MigrationStatus(migrationId), 2000); } } } catch (error) { console.error('Error polling migration status:', error); setTimeout(() => pollPgToS3MigrationStatus(migrationId), 5000); } } // ==================== Migration Status Functions ==================== async function loadMigrationsHandler() { setLoading(true); try { const postgresResult = await listPostgresMigrations(); const s3Result = await listS3Migrations(); const pgToS3Result = await listPgToS3Migrations(); migrations = []; if (postgresResult.success) migrations = migrations.concat(postgresResult.migrations || []); if (s3Result.success) migrations = migrations.concat(s3Result.migrations || []); if (pgToS3Result.success) migrations = migrations.concat(pgToS3Result.migrations || []); renderMigrations(); document.getElementById('migrationsList').style.display = 'block'; showNotification(`✅ Found ${migrations.length} migrations`, 'success'); } catch (error) { console.error('Error loading migrations:', error); showNotification(`❌ Error loading migrations: ${error.message}`, 'error'); } setLoading(false); } // ==================== Environment Handlers ==================== async function loadCurrentEnvHandler() { try { const result = await getCurrentEnv(); if (result.success) { currentEnv = result.environment_variables || {}; envPreview.textContent = formatEnvVars(currentEnv, 'dotenv'); renderEnvConfigCards(); showNotification('✅ Environment refreshed', 'success'); updateStatusInfo(); } } catch (error) { showNotification(`❌ Error loading environment: ${error.message}`, 'error'); } } // ==================== إضافة دوال التتبع إلى App() ==================== // أضف هذه الدوال داخل App() بعد دوال الـ polling الحالية /** * Track migration with real-time progress using Server-Sent Events * @param {string} migrationId - Migration ID to track * @param {string} type - Migration type ('postgres', 's3', 'postgres-s3') */ function trackMigrationWithProgress(migrationId, type) { showNotification(`📊 Tracking migration ${migrationId}...`, 'info'); // إنشاء منطقة عرض التقدم const progressContainer = document.createElement('div'); progressContainer.className = 'progress-modal'; progressContainer.innerHTML = ` `; document.body.appendChild(progressContainer); const closeBtn = progressContainer.querySelector('.close-modal'); closeBtn.addEventListener('click', () => progressContainer.remove()); // بدء تتبع التقدم const progressDiv = progressContainer.querySelector(`#progress-${migrationId}`); progressDiv.innerHTML = `
Connecting...
`; const progressBar = progressDiv.querySelector('.progress-bar-fill'); const statsDiv = progressDiv.querySelector('.progress-stats'); const detailsDiv = progressDiv.querySelector('.progress-details'); const stopBtn = progressDiv.querySelector('.stop-progress-btn'); // إنشاء تدفق التقدم const stream = createProgressStream(migrationId, type, { onProgress: (progress) => { // تحديث شريط التقدم const percentage = progress.percentage || progress.percentages?.size || 0; progressBar.style.width = `${percentage}%`; // تحديث الإحصائيات statsDiv.innerHTML = formatProgressDisplay(progress).replace(/\n/g, '
'); // تحديث التفاصيل let details = ''; if (progress.current_speed_formatted || progress.speed?.current_formatted) { details += `
⚡ Speed: ${progress.current_speed_formatted || progress.speed?.current_formatted}
`; } if (progress.eta_formatted || progress.time?.eta_formatted) { details += `
⏳ ETA: ${progress.eta_formatted || progress.time?.eta_formatted}
`; } if (progress.elapsed_time_formatted || progress.time?.elapsed_formatted) { details += `
⏱️ Elapsed: ${progress.elapsed_time_formatted || progress.time?.elapsed_formatted}
`; } detailsDiv.innerHTML = details; }, onComplete: (completion) => { statsDiv.innerHTML = '✅ Migration completed successfully!'; progressBar.style.width = '100%'; showNotification(`Migration ${migrationId} completed!`, 'success'); stopBtn.remove(); // تحديث قائمة الترحيلات loadMigrationsHandler(); }, onError: (error) => { statsDiv.innerHTML = `❌ Error: ${error.error}`; showNotification(`Migration error: ${error.error}`, 'error'); } }); stopBtn.addEventListener('click', () => { stream.stop(); progressContainer.remove(); showNotification('Progress tracking stopped', 'info'); }); } // ==================== تعديل دوال بدء الترحيل ==================== // استبدل دوال بدء الترحيل بهذه النسخ المعدلة: async function startPsqlToS3MigrationHandler() { if (!psqlToS3Config.source.uri && !psqlToS3Config.source.host) { showNotification('Please enter PostgreSQL connection details', 'warning'); return; } if (!psqlToS3Config.s3.bucket) { showNotification('Please enter S3 bucket name', 'warning'); return; } setLoading(true); try { const uri = psqlToS3Config.source.uri || buildPostgresConnectionString( psqlToS3Config.source.host, psqlToS3Config.source.database, psqlToS3Config.source.user, psqlToS3Config.source.port ); const result = await startPgToS3Migration( uri, psqlToS3Config.s3.bucket, psqlToS3Config.s3.prefix, { compress: psqlToS3Config.compress, format: psqlToS3Config.format, accessKeyId: psqlToS3Config.s3.accessKeyId, secretAccessKey: psqlToS3Config.s3.secretAccessKey, region: psqlToS3Config.s3.region, endpointUrl: psqlToS3Config.s3.endpointUrl } ); if (result.success) { activeMigration = result.migration_id; showNotification(`✅ PostgreSQL to S3 migration ${activeMigration} started!`, 'success'); // استخدام التدفق المباشر بدلاً من polling trackMigrationWithProgress(activeMigration, 'postgres-s3'); } else { showNotification(`❌ Failed to start migration: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error starting migration: ${error.message}`, 'error'); } setLoading(false); } async function startPsqlToPsqlMigrationHandler() { let sourceUri = psqlToPsqlConfig.source.uri || buildPostgresConnectionString( psqlToPsqlConfig.source.host, psqlToPsqlConfig.source.database, psqlToPsqlConfig.source.user, psqlToPsqlConfig.source.port ); let destUri = psqlToPsqlConfig.dest.uri || buildPostgresConnectionString( psqlToPsqlConfig.dest.host, psqlToPsqlConfig.dest.database, psqlToPsqlConfig.dest.user, psqlToPsqlConfig.dest.port ); if (!sourceUri) { showNotification('Please enter source PostgreSQL connection details', 'warning'); return; } if (!destUri) { showNotification('Please enter destination PostgreSQL connection details', 'warning'); return; } setLoading(true); try { const result = await startPostgresMigration( sourceUri, destUri, psqlToPsqlSelectedSchemas.length > 0 ? psqlToPsqlSelectedSchemas : null, psqlToPsqlSelectedTables.length > 0 ? psqlToPsqlSelectedTables : null ); if (result.success) { activeMigration = result.migration_id; showNotification(`✅ PostgreSQL migration ${activeMigration} started!`, 'success'); // استخدام التدفق المباشر بدلاً من polling trackMigrationWithProgress(activeMigration, 'postgres'); } else { showNotification(`❌ Failed to start migration: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error starting migration: ${error.message}`, 'error'); } setLoading(false); } async function startS3ToS3MigrationHandler() { if (!s3ToS3Config.source.bucket) { showNotification('Please select source bucket', 'warning'); return; } if (!s3ToS3Config.dest.bucket) { showNotification('Please select destination bucket', 'warning'); return; } setLoading(true); try { const includePatterns = document.getElementById('psqlToPsql_includePatterns')?.value ?.split(',').map(p => p.trim()).filter(p => p) || null; const excludePatterns = document.getElementById('psqlToPsql_excludePatterns')?.value ?.split(',').map(p => p.trim()).filter(p => p) || null; const result = await startS3Migration( s3ToS3Config.source.bucket, s3ToS3Config.dest.bucket, '', { sourceAccessKeyId: s3ToS3Config.source.accessKeyId, sourceSecretAccessKey: s3ToS3Config.source.secretAccessKey, sourceRegion: s3ToS3Config.source.region, sourceEndpointUrl: s3ToS3Config.source.endpointUrl, sourceSessionToken: s3ToS3Config.source.sessionToken, destAccessKeyId: s3ToS3Config.dest.accessKeyId, destSecretAccessKey: s3ToS3Config.dest.secretAccessKey, destRegion: s3ToS3Config.dest.region, destEndpointUrl: s3ToS3Config.dest.endpointUrl, destSessionToken: s3ToS3Config.dest.sessionToken, includePatterns, excludePatterns, preserveMetadata: document.getElementById('psqlToPsql_preserveMetadata')?.checked, storageClass: document.getElementById('psqlToPsql_storageClass')?.value, createDestBucket: document.getElementById('psqlToPsql_createDestBucket')?.checked, maxConcurrent: parseInt(document.getElementById('psqlToPsql_maxConcurrent')?.value) || 5 } ); if (result.success) { activeMigration = result.migration_id; showNotification(`✅ S3 to S3 migration ${activeMigration} started!`, 'success'); // استخدام التدفق المباشر بدلاً من polling trackMigrationWithProgress(activeMigration, 's3'); } else { showNotification(`❌ Failed to start migration: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error starting migration: ${error.message}`, 'error'); } setLoading(false); } async function injectEnvironmentHandler() { setLoading(true); try { const envVars = { // PSQL to PSQL Source SOURCE_PG_HOST: psqlToPsqlConfig.source.host, SOURCE_PG_PORT: psqlToPsqlConfig.source.port.toString(), SOURCE_PG_USER: psqlToPsqlConfig.source.user, SOURCE_PG_PASSWORD: psqlToPsqlConfig.source.password, SOURCE_PG_DATABASE: psqlToPsqlConfig.source.database, // PSQL to PSQL Destination DEST_PG_HOST: psqlToPsqlConfig.dest.host, DEST_PG_PORT: psqlToPsqlConfig.dest.port.toString(), DEST_PG_USER: psqlToPsqlConfig.dest.user, DEST_PG_PASSWORD: psqlToPsqlConfig.dest.password, DEST_PG_DATABASE: psqlToPsqlConfig.dest.database, // S3 Source SOURCE_AWS_ACCESS_KEY_ID: s3ToS3Config.source.accessKeyId, SOURCE_AWS_SECRET_ACCESS_KEY: s3ToS3Config.source.secretAccessKey, SOURCE_AWS_REGION: s3ToS3Config.source.region, SOURCE_AWS_ENDPOINT_URL: s3ToS3Config.source.endpointUrl, SOURCE_S3_BUCKET: s3ToS3Config.source.bucket, // S3 Destination DEST_AWS_ACCESS_KEY_ID: s3ToS3Config.dest.accessKeyId, DEST_AWS_SECRET_ACCESS_KEY: s3ToS3Config.dest.secretAccessKey, DEST_AWS_REGION: s3ToS3Config.dest.region, DEST_AWS_ENDPOINT_URL: s3ToS3Config.dest.endpointUrl, DEST_S3_BUCKET: s3ToS3Config.dest.bucket }; const result = await injectEnv(envVars); if (result.success) { showNotification(`✅ Injected ${result.injected_variables?.length || 0} environment variables`, 'success'); loadCurrentEnvHandler(); } else { showNotification(`❌ Failed to inject environment: ${result.error}`, 'error'); } } catch (error) { showNotification(`❌ Error injecting environment: ${error.message}`, 'error'); } setLoading(false); } async function clearEnvironmentHandler() { try { const result = await injectEnv({}); if (result.success) { showNotification('✅ Environment cleared', 'success'); loadCurrentEnvHandler(); } } catch (error) { showNotification(`❌ Error clearing environment: ${error.message}`, 'error'); } } async function copyEnvToClipboardHandler() { if (!currentEnv || Object.keys(currentEnv).length === 0) { showNotification('No environment variables to copy', 'warning'); return; } const format = document.getElementById('envFormat')?.value || 'dotenv'; const formatted = formatEnvVars(currentEnv, format); const success = await copyToClipboard(formatted); if (success) { showNotification(`✅ Copied ${Object.keys(currentEnv).length} variables to clipboard`, 'success'); } else { showNotification(`❌ Failed to copy to clipboard`, 'error'); } } // ==================== Security Handlers ==================== async function clearSessionHandler() { setLoading(true); try { const result = await clearSession(); if (result.success) { showNotification(`✅ ${result.message}`, 'success'); // Reset all configs psqlToS3Config = { source: { host: '', user: '', password: '', port: 5432, database: '', uri: '' }, s3: { accessKeyId: '', secretAccessKey: '', region: 'us-east-1', endpointUrl: '', bucket: '', prefix: '' }, compress: true, format: 'csv' }; psqlToPsqlConfig = { source: { host: '', user: '', password: '', port: 5432, database: '', uri: '' }, dest: { host: '', user: '', password: '', port: 5432, database: '', uri: '' } }; s3ToS3Config = { source: { accessKeyId: '', secretAccessKey: '', region: 'us-east-1', endpointUrl: '', sessionToken: '', bucket: 'my-source-bucket' }, dest: { accessKeyId: '', secretAccessKey: '', region: 'us-east-1', endpointUrl: '', sessionToken: '', bucket: 'my-destination-bucket' } }; currentEnv = {}; envPreview.textContent = ''; updateStatusInfo(); loadSecurityStatus(); } else { showNotification(`❌ Failed to clear session`, 'error'); } } catch (error) { showNotification(`❌ Error clearing session: ${error.message}`, 'error'); } setLoading(false); } async function clearMigrationHandler() { const migrationId = document.getElementById('cleanupMigrationId')?.value; if (!migrationId) { showNotification('Please enter migration ID', 'warning'); return; } setLoading(true); try { const result = await clearMigration(migrationId); if (result.success) { showNotification(`✅ ${result.message}`, 'success'); document.getElementById('cleanupMigrationId').value = ''; } else { showNotification(`❌ Failed to clear migration data`, 'error'); } } catch (error) { showNotification(`❌ Error clearing migration: ${error.message}`, 'error'); } setLoading(false); } async function loadSecurityStatus() { try { const result = await getSecurityStatus(); if (result.success) { document.getElementById('sessionId').textContent = result.security_status.current_session_id || 'Unknown'; document.getElementById('activeSessions').textContent = result.security_status.active_sessions || 0; } } catch (error) { console.error('Error loading security status:', error); } } // ==================== Render Functions ==================== function renderPsqlToS3Schemas(schemas) { const container = document.getElementById('psqlToS3_schemasContainer'); container.innerHTML = schemas.map(schema => `
`).join(''); } function renderPsqlToS3Tables(tables) { const container = document.getElementById('psqlToS3_tablesContainer'); const groups = groupTablesBySchema(tables); let html = ''; Object.entries(groups).forEach(([schema, group]) => { html += `

📚 Schema: ${schema}

${group.count} tables
${group.tables.map(table => ` `).join('')}
Table Name Type
${table.name} ${table.type}
`; }); container.innerHTML = html; } function renderPsqlToPsqlSchemas() { const container = document.getElementById('psqlToPsql_schemasContainer'); container.innerHTML = psqlToPsqlSchemas.map(schema => `
`).join(''); document.querySelectorAll('#psqlToPsql_schemasContainer .schema-checkbox').forEach(checkbox => { checkbox.addEventListener('change', (e) => { if (e.target.checked) { psqlToPsqlSelectedSchemas.push(e.target.value); } else { psqlToPsqlSelectedSchemas = psqlToPsqlSelectedSchemas.filter(s => s !== e.target.value); } }); }); } function renderPsqlToPsqlTables() { const container = document.getElementById('psqlToPsql_tablesContainer'); const groups = groupTablesBySchema(psqlToPsqlTables); let html = ''; Object.entries(groups).forEach(([schema, group]) => { html += `

📚 Schema: ${schema}

${group.count} tables
${group.tables.map(table => ` `).join('')}
Table Name Type
${table.name} ${table.type}
`; }); container.innerHTML = html; document.querySelectorAll('#psqlToPsql_tablesContainer .table-checkbox').forEach(checkbox => { checkbox.addEventListener('change', (e) => { const tableName = e.target.value; if (e.target.checked) { if (!psqlToPsqlSelectedTables.includes(tableName)) { psqlToPsqlSelectedTables.push(tableName); } } else { psqlToPsqlSelectedTables = psqlToPsqlSelectedTables.filter(t => t !== tableName); } }); }); } function renderS3ToS3SourceBuckets() { const container = document.getElementById('s3ToS3_sourceBucketsContainer'); container.innerHTML = s3ToS3SourceBuckets.map(bucket => `
${bucket.name}
Region: ${bucket.region} Created: ${new Date(bucket.creation_date).toLocaleDateString()} Objects: ${bucket.object_count} Size: ${formatFileSize(bucket.total_size)}
`).join(''); document.querySelectorAll('#s3ToS3_sourceBucketsContainer .select-bucket-btn').forEach(btn => { btn.addEventListener('click', (e) => { const bucketName = e.target.dataset.bucket; s3ToS3Config.source.bucket = bucketName; document.getElementById('s3ToS3_sourceBucket').value = bucketName; updateS3Summaries(); showNotification(`✅ Selected source bucket: ${bucketName}`, 'success'); document.getElementById('s3ToS3_sourceBucketsList').style.display = 'none'; }); }); } function renderS3ToS3DestBuckets() { const container = document.getElementById('s3ToS3_destBucketsContainer'); container.innerHTML = s3ToS3DestBuckets.map(bucket => `
${bucket.name}
Region: ${bucket.region} Created: ${new Date(bucket.creation_date).toLocaleDateString()} Objects: ${bucket.object_count} Size: ${formatFileSize(bucket.total_size)}
`).join(''); document.querySelectorAll('#s3ToS3_destBucketsContainer .select-bucket-btn').forEach(btn => { btn.addEventListener('click', (e) => { const bucketName = e.target.dataset.bucket; s3ToS3Config.dest.bucket = bucketName; document.getElementById('s3ToS3_destBucket').value = bucketName; updateS3Summaries(); showNotification(`✅ Selected destination bucket: ${bucketName}`, 'success'); document.getElementById('s3ToS3_destBucketsList').style.display = 'none'; }); }); } function renderMigrations() { const container = document.getElementById('migrationsContainer'); if (!container) return; if (migrations.length === 0) { container.innerHTML = '

No migrations found

'; return; } container.innerHTML = migrations.map(migration => `
${migration.id} ${migration.status}

Status: ${migration.status}

Started: ${migration.started_at ? new Date(migration.started_at * 1000).toLocaleString() : 'N/A'}

${migration.status === 'running' ? ` ` : ''}
`).join(''); document.querySelectorAll('.view-logs-btn').forEach(btn => { btn.addEventListener('click', async (e) => { const migrationId = e.target.dataset.id; setActiveMigration(migrationId); }); }); } function renderMigrationLogs() { const container = document.getElementById('logsContainer'); if (!container) return; if (!migrationLogs || migrationLogs.length === 0) { container.innerHTML = '

No logs available

'; return; } container.innerHTML = migrationLogs.map(log => `
${log.timestamp ? new Date(log.timestamp).toLocaleTimeString() : ''} ${log.message}
`).join(''); } function renderEnvConfigCards() { const container = document.getElementById('envConfigCards'); if (!container) return; container.innerHTML = `

🐘 PSQL to PSQL Source

${formatPostgresConfig(psqlToPsqlConfig.source)}

🐘 PSQL to PSQL Dest

${formatPostgresConfig(psqlToPsqlConfig.dest)}

📤 S3 to S3 Source

${formatS3Config({ 
          endpoint_url: s3ToS3Config.source.endpointUrl,
          region: s3ToS3Config.source.region,
          bucket: s3ToS3Config.source.bucket,
          access_key_id: s3ToS3Config.source.accessKeyId,
          secret_access_key: s3ToS3Config.source.secretAccessKey
        }, 'source')}

📥 S3 to S3 Dest

${formatS3Config({ 
          endpoint_url: s3ToS3Config.dest.endpointUrl,
          region: s3ToS3Config.dest.region,
          bucket: s3ToS3Config.dest.bucket,
          access_key_id: s3ToS3Config.dest.accessKeyId,
          secret_access_key: s3ToS3Config.dest.secretAccessKey
        }, 'destination')}
`; } // ==================== Additional Handlers ==================== async function setActiveMigration(migrationId) { activeMigration = migrationId; setLoading(true); try { let result = await getPostgresMigrationStatus(migrationId); if (!result.success) { result = await getS3MigrationStatus(migrationId); } if (!result.success) { result = await getPgToS3MigrationStatus(migrationId); } if (result.success && result.status) { // Switch to environment tab where migrations are shown document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); document.querySelector('[data-tab="environment"]').classList.add('active'); document.getElementById('tab-environment').classList.add('active'); if (result.status.logs && Array.isArray(result.status.logs)) { migrationLogs = result.status.logs; renderMigrationLogs(); document.getElementById('migrationLogs').style.display = 'block'; } showNotification(`✅ Loaded logs for migration ${migrationId}`, 'success'); } } catch (error) { console.error('Error fetching migration logs:', error); showNotification(`❌ Error loading migration logs: ${error.message}`, 'error'); } setLoading(false); } // ==================== Initialize ==================== function init() { setupEventListeners(); // Set initial summaries updatePostgresSummaries(); updateS3Summaries(); // Load initial data loadCurrentEnvHandler(); loadSecurityStatus(); updateStatusInfo(); } init(); } // ==================== Start the app ==================== document.addEventListener("DOMContentLoaded", App);