الملفات
psqlmigrator/index.js

3366 أسطر
124 KiB
JavaScript
خام اللوم التاريخ

هذا الملف يحتوي على أحرف Unicode غير مرئية

هذا الملف يحتوي على أحرف Unicode غير مرئية لا يمكن التمييز بينها بعين الإنسان ولكن قد تتم معالجتها بشكل مختلف بواسطة الحاسوب. إذا كنت تعتقد أن هذا مقصود، يمكنك تجاهل هذا التحذير بأمان. استخدم زر الهروب للكشف عنها.

هذا الملف يحتوي على أحرف Unicode قد تُخلط مع أحرف أخرى. إذا كنت تعتقد أن هذا مقصود، يمكنك تجاهل هذا التحذير بأمان. استخدم زر الهروب للكشف عنها.

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 = `
<span class="notification-icon">${type === 'success' ? '✅' : type === 'error' ? '❌' : type === 'warning' ? '⚠️' : ''}</span>
<span class="notification-message">${message}</span>
<button class="notification-close" onclick="this.parentElement.remove()">×</button>
`;
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 = `
<div class="container">
<!-- Notifications Container -->
<div id="notifications"></div>
<header class="header">
<div class="header-content">
<h1>🔄 Universal Migrator</h1>
<p>PostgreSQL & S3 Migration Tool</p>
</div>
<div class="header-actions">
<div class="security-badge">🔒 Secure Mode</div>
</div>
</header>
<!-- Simplified Tabs - 4 tabs only -->
<div class="tabs">
<button class="tab-btn active" data-tab="psql-to-s3">🐘 → 📤 PSQL to S3</button>
<button class="tab-btn" data-tab="psql-to-psql">🐘 → 🐘 PSQL to PSQL</button>
<button class="tab-btn" data-tab="s3-to-s3">📤 → 📥 S3 to S3</button>
<button class="tab-btn" data-tab="environment">⚙️ Environment</button>
</div>
<!-- ==================== PSQL to S3 Tab ==================== -->
<div class="tab-content active" id="tab-psql-to-s3">
<h2>🐘 → 📤 PostgreSQL to S3 Migration</h2>
<div class="migration-layout">
<!-- Left Column - PostgreSQL Configuration -->
<div class="migration-column">
<div class="config-section">
<h3>PostgreSQL Source</h3>
<div class="config-group">
<label>Host:</label>
<input type="text" id="psqlToS3_sourceHost" placeholder="localhost">
</div>
<div class="config-group">
<label>Port:</label>
<input type="number" id="psqlToS3_sourcePort" value="5432">
</div>
<div class="config-group">
<label>User:</label>
<input type="text" id="psqlToS3_sourceUser" placeholder="postgres">
</div>
<div class="config-group">
<label>Password:</label>
<input type="password" id="psqlToS3_sourcePassword" placeholder="••••••••">
</div>
<div class="config-group">
<label>Database:</label>
<input type="text" id="psqlToS3_sourceDatabase" placeholder="postgres">
</div>
<div class="uri-input-group">
<label>Or paste URI:</label>
<div class="uri-input-wrapper">
<input type="text" id="psqlToS3_sourceUri" placeholder="postgresql://user:pass@host:5432/database">
<button id="psqlToS3_parseSourceUri" class="btn-small">Parse</button>
</div>
</div>
<div class="action-group">
<button id="psqlToS3_testSourceConnection" class="btn-test">Test Connection</button>
<button id="psqlToS3_getSchemas" class="btn-secondary">📚 Get Schemas</button>
<button id="psqlToS3_getTables" class="btn-secondary">📊 Get Tables</button>
</div>
<div id="psqlToS3_sourceConnectionStatus" style="display:none;" class="status-message"></div>
</div>
<div id="psqlToS3_schemasSection" style="display:none;" class="results-section">
<h4>Available Schemas</h4>
<div id="psqlToS3_schemasContainer" class="schemas-grid"></div>
</div>
<div id="psqlToS3_tablesSection" style="display:none;" class="results-section">
<h4>Tables</h4>
<div id="psqlToS3_tablesContainer"></div>
</div>
</div>
<!-- Right Column - S3 Configuration -->
<div class="migration-column">
<div class="config-section">
<h3>S3 Destination</h3>
<div class="config-group">
<label>Access Key ID:</label>
<input type="password" id="psqlToS3_s3AccessKeyId">
</div>
<div class="config-group">
<label>Secret Access Key:</label>
<input type="password" id="psqlToS3_s3SecretAccessKey">
</div>
<div class="config-group">
<label>Region:</label>
<input type="text" id="psqlToS3_s3Region" value="us-east-1">
</div>
<div class="config-group">
<label>Endpoint URL:</label>
<input type="text" id="psqlToS3_s3EndpointUrl" placeholder="Optional">
</div>
<div class="config-group">
<label>S3 Bucket:</label>
<input type="text" id="psqlToS3_s3Bucket" placeholder="my-s3-bucket">
</div>
<div class="config-group">
<label>S3 Prefix:</label>
<input type="text" id="psqlToS3_s3Prefix" placeholder="backups/">
</div>
<div class="config-row">
<label class="checkbox-label">
<input type="checkbox" id="psqlToS3_compress" checked> Compress (GZIP)
</label>
<div class="select-wrapper">
<select id="psqlToS3_format">
<option value="csv">CSV</option>
<option value="json">JSON</option>
</select>
</div>
</div>
<div class="action-group">
<button id="psqlToS3_testPgConnection" class="btn-test">Test PG Connection</button>
<button id="psqlToS3_testS3Connection" class="btn-test">Test S3 Connection</button>
</div>
<div class="migration-controls">
<button id="psqlToS3_startMigration" class="btn-migrate btn-large">🚀 Start PSQL to S3 Migration</button>
</div>
</div>
</div>
</div>
</div>
<!-- ==================== PSQL to PSQL Tab ==================== -->
<div class="tab-content" id="tab-psql-to-psql">
<h2>🐘 → 🐘 PostgreSQL to PostgreSQL Migration</h2>
<div class="migration-layout">
<!-- Left Column - Source PostgreSQL -->
<div class="migration-column">
<div class="config-section">
<h3>Source PostgreSQL</h3>
<div class="config-group">
<label>Host:</label>
<input type="text" id="psqlToPsql_sourceHost" placeholder="localhost">
</div>
<div class="config-group">
<label>Port:</label>
<input type="number" id="psqlToPsql_sourcePort" value="5432">
</div>
<div class="config-group">
<label>User:</label>
<input type="text" id="psqlToPsql_sourceUser" placeholder="postgres">
</div>
<div class="config-group">
<label>Password:</label>
<input type="password" id="psqlToPsql_sourcePassword" placeholder="••••••••">
</div>
<div class="config-group">
<label>Database:</label>
<input type="text" id="psqlToPsql_sourceDatabase" placeholder="postgres">
</div>
<div class="uri-input-group">
<label>Or paste URI:</label>
<div class="uri-input-wrapper">
<input type="text" id="psqlToPsql_sourceUri" placeholder="postgresql://user:pass@host:5432/database">
<button id="psqlToPsql_parseSourceUri" class="btn-small">Parse</button>
</div>
</div>
<div class="action-group">
<button id="psqlToPsql_testSourceConnection" class="btn-test">Test Connection</button>
<button id="psqlToPsql_getSchemas" class="btn-secondary">📚 Get Schemas</button>
<button id="psqlToPsql_getTables" class="btn-secondary">📊 Get Tables</button>
</div>
<div id="psqlToPsql_sourceConnectionStatus" style="display:none;" class="status-message"></div>
</div>
<div id="psqlToPsql_schemasSection" style="display:none;" class="results-section">
<h4>Available Schemas</h4>
<div id="psqlToPsql_schemasContainer" class="schemas-grid"></div>
</div>
<div id="psqlToPsql_tablesSection" style="display:none;" class="results-section">
<h4>Tables</h4>
<div id="psqlToPsql_tablesContainer"></div>
</div>
</div>
<!-- Right Column - Destination PostgreSQL -->
<div class="migration-column">
<div class="config-section">
<h3>Destination PostgreSQL</h3>
<div class="config-group">
<label>Host:</label>
<input type="text" id="psqlToPsql_destHost" placeholder="localhost">
</div>
<div class="config-group">
<label>Port:</label>
<input type="number" id="psqlToPsql_destPort" value="5432">
</div>
<div class="config-group">
<label>User:</label>
<input type="text" id="psqlToPsql_destUser" placeholder="postgres">
</div>
<div class="config-group">
<label>Password:</label>
<input type="password" id="psqlToPsql_destPassword" placeholder="••••••••">
</div>
<div class="config-group">
<label>Database:</label>
<input type="text" id="psqlToPsql_destDatabase" placeholder="postgres">
</div>
<div class="uri-input-group">
<label>Or paste URI:</label>
<div class="uri-input-wrapper">
<input type="text" id="psqlToPsql_destUri" placeholder="postgresql://user:pass@host:5432/database">
<button id="psqlToPsql_parseDestUri" class="btn-small">Parse</button>
</div>
</div>
<div class="action-group">
<button id="psqlToPsql_testDestConnection" class="btn-test">Test Connection</button>
</div>
<div id="psqlToPsql_destConnectionStatus" style="display:none;" class="status-message"></div>
</div>
</div>
</div>
<!-- Migration Options and Controls -->
<div class="migration-options-panel">
<h3>Migration Options</h3>
<div class="options-row">
<label class="checkbox-label">
<input type="checkbox" id="psqlToPsql_createDestBucket" checked> Create Destination Bucket if Missing
</label>
<label class="checkbox-label">
<input type="checkbox" id="psqlToPsql_preserveMetadata" checked> Preserve Metadata
</label>
</div>
<div class="options-row">
<div class="option-item">
<label>Storage Class:</label>
<select id="psqlToPsql_storageClass">
<option value="STANDARD">Standard</option>
<option value="STANDARD_IA">Standard-IA</option>
<option value="INTELLIGENT_TIERING">Intelligent-Tiering</option>
<option value="GLACIER">Glacier</option>
</select>
</div>
<div class="option-item">
<label>Max Concurrent:</label>
<input type="number" id="psqlToPsql_maxConcurrent" value="5" min="1" max="20">
</div>
</div>
<div class="options-row full-width">
<div class="option-item">
<label>Include Patterns:</label>
<input type="text" id="psqlToPsql_includePatterns" placeholder="*.csv, *.json, important/*">
</div>
<div class="option-item">
<label>Exclude Patterns:</label>
<input type="text" id="psqlToPsql_excludePatterns" placeholder="*.tmp, *.log, temp/*">
</div>
</div>
<div class="migration-controls">
<button id="psqlToPsql_startMigration" class="btn-migrate btn-large">🚀 Start PSQL to PSQL Migration</button>
</div>
</div>
</div>
<!-- ==================== S3 to S3 Tab ==================== -->
<div class="tab-content" id="tab-s3-to-s3">
<h2>📤 → 📥 S3 to S3 Migration</h2>
<div class="migration-layout">
<!-- Left Column - Source S3 -->
<div class="migration-column">
<div class="config-section">
<h3>Source S3</h3>
<div class="config-group">
<label>Access Key ID:</label>
<input type="password" id="s3ToS3_sourceAccessKeyId">
</div>
<div class="config-group">
<label>Secret Access Key:</label>
<input type="password" id="s3ToS3_sourceSecretAccessKey">
</div>
<div class="config-group">
<label>Region:</label>
<input type="text" id="s3ToS3_sourceRegion" value="us-east-1">
</div>
<div class="config-group">
<label>Endpoint URL:</label>
<input type="text" id="s3ToS3_sourceEndpointUrl" placeholder="Optional">
</div>
<div class="config-group">
<label>Session Token:</label>
<input type="password" id="s3ToS3_sourceSessionToken" placeholder="Optional">
</div>
<div class="config-group">
<label>Bucket:</label>
<input type="text" id="s3ToS3_sourceBucket" placeholder="source-bucket" value="my-source-bucket">
</div>
<div class="action-group">
<button id="s3ToS3_testSourceConnection" class="btn-test">Test Connection</button>
<button id="s3ToS3_listSourceBuckets" class="btn-secondary">📁 List Buckets</button>
</div>
<div id="s3ToS3_sourceConnectionStatus" style="display:none;" class="status-message"></div>
</div>
<div id="s3ToS3_sourceBucketsList" style="display:none;" class="results-section">
<h4>Available Buckets:</h4>
<div id="s3ToS3_sourceBucketsContainer"></div>
<button id="s3ToS3_hideSourceBuckets" class="btn-small">Hide</button>
</div>
</div>
<!-- Right Column - Destination S3 -->
<div class="migration-column">
<div class="config-section">
<h3>Destination S3</h3>
<div class="config-group">
<label>Access Key ID:</label>
<input type="password" id="s3ToS3_destAccessKeyId">
</div>
<div class="config-group">
<label>Secret Access Key:</label>
<input type="password" id="s3ToS3_destSecretAccessKey">
</div>
<div class="config-group">
<label>Region:</label>
<input type="text" id="s3ToS3_destRegion" value="us-east-1">
</div>
<div class="config-group">
<label>Endpoint URL:</label>
<input type="text" id="s3ToS3_destEndpointUrl" placeholder="Optional">
</div>
<div class="config-group">
<label>Session Token:</label>
<input type="password" id="s3ToS3_destSessionToken" placeholder="Optional">
</div>
<div class="config-group">
<label>Bucket:</label>
<input type="text" id="s3ToS3_destBucket" placeholder="destination-bucket" value="my-destination-bucket">
</div>
<div class="action-group">
<button id="s3ToS3_testDestConnection" class="btn-test">Test Connection</button>
<button id="s3ToS3_listDestBuckets" class="btn-secondary">📁 List Buckets</button>
<button id="s3ToS3_createDestBucket" class="btn-success"> Create Bucket</button>
</div>
<div id="s3ToS3_destConnectionStatus" style="display:none;" class="status-message"></div>
</div>
<div id="s3ToS3_destBucketsList" style="display:none;" class="results-section">
<h4>Available Buckets:</h4>
<div id="s3ToS3_destBucketsContainer"></div>
<button id="s3ToS3_hideDestBuckets" class="btn-small">Hide</button>
</div>
</div>
</div>
<!-- Migration Controls -->
<div class="migration-controls-panel">
<button id="s3ToS3_startMigration" class="btn-migrate btn-large">🚀 Start S3 to S3 Migration</button>
</div>
</div>
<!-- ==================== Environment Tab ==================== -->
<div class="tab-content" id="tab-environment">
<h2>⚙️ Environment & Security</h2>
<div class="environment-layout">
<!-- Left Column - Environment Variables -->
<div class="environment-column">
<div class="config-section">
<h3>Environment Variables</h3>
<div class="action-group">
<button id="refreshEnv" class="btn-secondary">🔄 Refresh</button>
<button id="injectEnv" class="btn-primary">⚡ Inject</button>
<button id="clearEnv" class="btn-warning">🗑️ Clear</button>
</div>
<div class="format-controls">
<select id="envFormat">
<option value="dotenv">.env file</option>
<option value="json">JSON</option>
<option value="shell_export">Shell export</option>
<option value="docker_env">Docker -e flags</option>
<option value="docker_compose">Docker Compose</option>
</select>
<button id="copyEnv" class="btn-secondary">📋 Copy</button>
</div>
</div>
<div class="config-section">
<h3>Current Configuration</h3>
<div id="envConfigCards" class="config-cards"></div>
</div>
<div class="config-section">
<h3>Environment Preview</h3>
<pre id="envPreview" class="env-preview"></pre>
</div>
</div>
<!-- Right Column - Security & Migrations -->
<div class="environment-column">
<div class="config-section">
<h3>Security</h3>
<div class="security-info">
<p><strong>Session ID:</strong> <span id="sessionId">Loading...</span></p>
<p><strong>Active Sessions:</strong> <span id="activeSessions">-</span></p>
<p><strong>Expiry:</strong> 10 minutes</p>
</div>
<div class="security-features">
<div class="security-feature">
<span>Encryption:</span>
<span class="feature-value">AES-256</span>
</div>
<div class="security-feature">
<span>Session Isolation:</span>
<span class="feature-value">✅ Enabled</span>
</div>
<div class="security-feature">
<span>Auto-cleanup:</span>
<span class="feature-value">10 minutes</span>
</div>
</div>
<div class="action-group">
<button id="clearSessionBtn" class="btn-warning">🗑️ Clear Session</button>
</div>
</div>
<div class="config-section">
<h3>Migration Data Cleanup</h3>
<p>Clear sensitive data from migrations</p>
<div class="cleanup-controls">
<input type="text" id="cleanupMigrationId" placeholder="Migration ID">
<button id="clearMigrationBtn" class="btn-warning">🧹 Clear</button>
</div>
</div>
<div class="config-section">
<h3>Migration History</h3>
<div class="section-header">
<button id="refreshMigrations" class="btn-secondary">🔄 Refresh</button>
</div>
<div id="migrationStats" style="display:none;" class="migration-stats">
<h4>Statistics</h4>
<div id="statsGrid" class="stats-grid"></div>
</div>
<div id="migrationsList" style="display:none;">
<div id="migrationsContainer"></div>
</div>
<div id="migrationLogs" style="display:none;">
<h4>Logs</h4>
<div id="logsContainer"></div>
</div>
</div>
</div>
</div>
</div>
<div class="status-bar">
<div id="statusInfo">Ready</div>
<div id="loading" style="display:none;">⏳ Processing...</div>
</div>
</div>
`;
// ==================== 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 = `
<p><strong>Success:</strong> ✅ Connected</p>
<p><strong>Host:</strong> ${psqlToS3Config.source.host || result.connection?.host}:${psqlToS3Config.source.port || result.connection?.port}</p>
<p><strong>Version:</strong> ${result.version || 'Unknown'}</p>
`;
statusDiv.className = 'status-message success';
} else {
showNotification(`❌ PSQL to S3 - Source connection failed: ${result.error}`, 'error');
statusDiv.innerHTML = `<p><strong>Error:</strong> ${result.error}</p>`;
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 = `
<p><strong>Success:</strong> ✅ Connected</p>
<p><strong>Host:</strong> ${psqlToPsqlConfig.source.host || result.connection?.host}:${psqlToPsqlConfig.source.port || result.connection?.port}</p>
<p><strong>Version:</strong> ${result.version || 'Unknown'}</p>
`;
statusDiv.className = 'status-message success';
} else {
showNotification(`❌ PSQL to PSQL - Source connection failed: ${result.error}`, 'error');
statusDiv.innerHTML = `<p><strong>Error:</strong> ${result.error}</p>`;
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 = `
<p><strong>Success:</strong> ✅ Connected</p>
<p><strong>Host:</strong> ${psqlToPsqlConfig.dest.host || result.connection?.host}:${psqlToPsqlConfig.dest.port || result.connection?.port}</p>
<p><strong>Version:</strong> ${result.version || 'Unknown'}</p>
`;
statusDiv.className = 'status-message success';
} else {
showNotification(`❌ PSQL to PSQL - Destination connection failed: ${result.error}`, 'error');
statusDiv.innerHTML = `<p><strong>Error:</strong> ${result.error}</p>`;
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 = `
<p><strong>Success:</strong> ✅ Connected</p>
<p><strong>Endpoint:</strong> ${s3ToS3Config.source.endpointUrl || 'AWS S3 (default)'}</p>
<p><strong>Region:</strong> ${s3ToS3Config.source.region}</p>
<p><strong>Buckets Found:</strong> ${result.bucket_count || 0}</p>
`;
statusDiv.className = 'status-message success';
} else {
showNotification(`❌ S3 to S3 - Source connection failed: ${result.error}`, 'error');
statusDiv.innerHTML = `<p><strong>Error:</strong> ${result.error}</p>`;
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 = `
<p><strong>Success:</strong> ✅ Connected</p>
<p><strong>Endpoint:</strong> ${s3ToS3Config.dest.endpointUrl || 'AWS S3 (default)'}</p>
<p><strong>Region:</strong> ${s3ToS3Config.dest.region}</p>
<p><strong>Buckets Found:</strong> ${result.bucket_count || 0}</p>
`;
statusDiv.className = 'status-message success';
} else {
showNotification(`❌ S3 to S3 - Destination connection failed: ${result.error}`, 'error');
statusDiv.innerHTML = `<p><strong>Error:</strong> ${result.error}</p>`;
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 = `
<div class="modal-content">
<div class="modal-header">
<h3>Migration Progress: ${migrationId}</h3>
<button class="close-modal">&times;</button>
</div>
<div class="modal-body" id="progress-${migrationId}"></div>
</div>
`;
document.body.appendChild(progressContainer);
const closeBtn = progressContainer.querySelector('.close-modal');
closeBtn.addEventListener('click', () => progressContainer.remove());
// بدء تتبع التقدم
const progressDiv = progressContainer.querySelector(`#progress-${migrationId}`);
progressDiv.innerHTML = `
<div class="progress-container">
<div class="progress-bar-container">
<div class="progress-bar-fill" style="width: 0%"></div>
</div>
<div class="progress-stats">Connecting...</div>
<div class="progress-details"></div>
<button class="stop-progress-btn">Stop Tracking</button>
</div>
`;
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, '<br>');
// تحديث التفاصيل
let details = '';
if (progress.current_speed_formatted || progress.speed?.current_formatted) {
details += `<div>⚡ Speed: ${progress.current_speed_formatted || progress.speed?.current_formatted}</div>`;
}
if (progress.eta_formatted || progress.time?.eta_formatted) {
details += `<div>⏳ ETA: ${progress.eta_formatted || progress.time?.eta_formatted}</div>`;
}
if (progress.elapsed_time_formatted || progress.time?.elapsed_formatted) {
details += `<div>⏱️ Elapsed: ${progress.elapsed_time_formatted || progress.time?.elapsed_formatted}</div>`;
}
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 => `
<div class="schema-card">
<label>
<input type="checkbox" class="schema-checkbox" value="${schema}">
${schema}
</label>
</div>
`).join('');
}
function renderPsqlToS3Tables(tables) {
const container = document.getElementById('psqlToS3_tablesContainer');
const groups = groupTablesBySchema(tables);
let html = '';
Object.entries(groups).forEach(([schema, group]) => {
html += `
<div class="table-group">
<h4>📚 Schema: ${schema}</h4>
<div class="group-stats">
<span>${group.count} tables</span>
</div>
<div class="table-responsive">
<table class="tables-table">
<thead>
<tr>
<th style="width: 30px">
<input type="checkbox" class="schema-select-all" data-schema="${schema}">
</th>
<th>Table Name</th>
<th>Type</th>
</tr>
</thead>
<tbody>
${group.tables.map(table => `
<tr>
<td>
<input type="checkbox" class="table-checkbox" value="${table.name}" data-schema="${schema}">
</td>
<td>${table.name}</td>
<td>${table.type}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
`;
});
container.innerHTML = html;
}
function renderPsqlToPsqlSchemas() {
const container = document.getElementById('psqlToPsql_schemasContainer');
container.innerHTML = psqlToPsqlSchemas.map(schema => `
<div class="schema-card">
<label>
<input type="checkbox" class="schema-checkbox" value="${schema}">
${schema}
</label>
</div>
`).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 += `
<div class="table-group">
<h4>📚 Schema: ${schema}</h4>
<div class="group-stats">
<span>${group.count} tables</span>
</div>
<div class="table-responsive">
<table class="tables-table">
<thead>
<tr>
<th style="width: 30px">
<input type="checkbox" class="schema-select-all" data-schema="${schema}">
</th>
<th>Table Name</th>
<th>Type</th>
</tr>
</thead>
<tbody>
${group.tables.map(table => `
<tr>
<td>
<input type="checkbox" class="table-checkbox" value="${table.name}" data-schema="${schema}">
</td>
<td>${table.name}</td>
<td>${table.type}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
`;
});
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 => `
<div class="bucket-card" data-bucket="${bucket.name}">
<div class="bucket-name">${bucket.name}</div>
<div class="bucket-details">
<span class="bucket-region">Region: ${bucket.region}</span>
<span class="bucket-created">Created: ${new Date(bucket.creation_date).toLocaleDateString()}</span>
<span class="bucket-size">Objects: ${bucket.object_count}</span>
<span class="bucket-size">Size: ${formatFileSize(bucket.total_size)}</span>
</div>
<button class="select-bucket-btn" data-bucket="${bucket.name}">Select</button>
</div>
`).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 => `
<div class="bucket-card" data-bucket="${bucket.name}">
<div class="bucket-name">${bucket.name}</div>
<div class="bucket-details">
<span class="bucket-region">Region: ${bucket.region}</span>
<span class="bucket-created">Created: ${new Date(bucket.creation_date).toLocaleDateString()}</span>
<span class="bucket-size">Objects: ${bucket.object_count}</span>
<span class="bucket-size">Size: ${formatFileSize(bucket.total_size)}</span>
</div>
<button class="select-bucket-btn" data-bucket="${bucket.name}">Select</button>
</div>
`).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 = '<p class="no-migrations">No migrations found</p>';
return;
}
container.innerHTML = migrations.map(migration => `
<div class="migration-card">
<div class="migration-header">
<span class="migration-id">${migration.id}</span>
<span class="migration-status ${migration.status}">${migration.status}</span>
</div>
<div class="migration-body">
<p><strong>Status:</strong> ${migration.status}</p>
<p><strong>Started:</strong> ${migration.started_at ? new Date(migration.started_at * 1000).toLocaleString() : 'N/A'}</p>
</div>
<div class="migration-actions">
<button class="view-logs-btn" data-id="${migration.id}">View Logs</button>
${migration.status === 'running' ? `
<button class="cancel-migration-btn" data-id="${migration.id}">Cancel</button>
` : ''}
</div>
</div>
`).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 = '<p class="no-logs">No logs available</p>';
return;
}
container.innerHTML = migrationLogs.map(log => `
<div class="log-entry ${log.level || 'info'}">
<span class="log-time">${log.timestamp ? new Date(log.timestamp).toLocaleTimeString() : ''}</span>
<span class="log-message">${log.message}</span>
</div>
`).join('');
}
function renderEnvConfigCards() {
const container = document.getElementById('envConfigCards');
if (!container) return;
container.innerHTML = `
<div class="config-card">
<h4>🐘 PSQL to PSQL Source</h4>
<pre>${formatPostgresConfig(psqlToPsqlConfig.source)}</pre>
</div>
<div class="config-card">
<h4>🐘 PSQL to PSQL Dest</h4>
<pre>${formatPostgresConfig(psqlToPsqlConfig.dest)}</pre>
</div>
<div class="config-card">
<h4>📤 S3 to S3 Source</h4>
<pre>${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')}</pre>
</div>
<div class="config-card">
<h4>📥 S3 to S3 Dest</h4>
<pre>${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')}</pre>
</div>
`;
}
// ==================== 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);