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 = ` `; 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 = `
PostgreSQL & S3 Migration Tool
Session ID: Loading...
Active Sessions: -
Expiry: 10 minutes
Clear sensitive data from migrations
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 = `| Table Name | Type | |
|---|---|---|
| ${table.name} | ${table.type} |
| Table Name | Type | |
|---|---|---|
| ${table.name} | ${table.type} |
No migrations found
'; return; } container.innerHTML = migrations.map(migration => `Status: ${migration.status}
Started: ${migration.started_at ? new Date(migration.started_at * 1000).toLocaleString() : 'N/A'}
No logs available
'; return; } container.innerHTML = migrationLogs.map(log => `${formatPostgresConfig(psqlToPsqlConfig.source)}
${formatPostgresConfig(psqlToPsqlConfig.dest)}
${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')}
${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')}