Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -15,6 +15,10 @@ from typing import Dict, Any, Optional, List
|
|
| 15 |
from pathlib import Path
|
| 16 |
import psycopg2
|
| 17 |
import logging
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
# Setup logging
|
| 20 |
logging.basicConfig(
|
|
@@ -172,6 +176,13 @@ def run_dump(source_conn: str, file_path: str, options: dict):
|
|
| 172 |
migration_state["dump_completed"] = False
|
| 173 |
migration_state["previous_size"] = 0 # Reset previous size
|
| 174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
process = subprocess.Popen(
|
| 176 |
cmd,
|
| 177 |
stdout=subprocess.PIPE,
|
|
@@ -179,7 +190,8 @@ def run_dump(source_conn: str, file_path: str, options: dict):
|
|
| 179 |
env=env,
|
| 180 |
text=True,
|
| 181 |
bufsize=1, # Line buffering
|
| 182 |
-
universal_newlines=True
|
|
|
|
| 183 |
)
|
| 184 |
|
| 185 |
with migration_lock:
|
|
@@ -295,6 +307,11 @@ def run_restore(target_conn: str, file_path: str, options: dict):
|
|
| 295 |
migration_state["restore_completed"] = False
|
| 296 |
migration_state["progress"]["tables_completed"] = 0 # Reset counter
|
| 297 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
process = subprocess.Popen(
|
| 299 |
cmd,
|
| 300 |
stdout=subprocess.PIPE,
|
|
@@ -302,7 +319,8 @@ def run_restore(target_conn: str, file_path: str, options: dict):
|
|
| 302 |
env=env,
|
| 303 |
text=True,
|
| 304 |
bufsize=1, # Line buffering
|
| 305 |
-
universal_newlines=True
|
|
|
|
| 306 |
)
|
| 307 |
|
| 308 |
with migration_lock:
|
|
@@ -388,37 +406,98 @@ def run_restore(target_conn: str, file_path: str, options: dict):
|
|
| 388 |
migration_state["process"] = None
|
| 389 |
return False
|
| 390 |
|
|
|
|
| 391 |
def stop_current_process():
|
| 392 |
-
"""Stop the current process
|
| 393 |
with migration_lock:
|
| 394 |
if migration_state["process"] and migration_state["running"]:
|
| 395 |
try:
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
migration_state["process"].terminate() # Send SIGTERM
|
| 399 |
-
try:
|
| 400 |
-
# Wait a short time for graceful termination
|
| 401 |
-
migration_state["process"].wait(timeout=2)
|
| 402 |
-
except subprocess.TimeoutExpired:
|
| 403 |
-
logger.warning(f"Process {pid} did not terminate gracefully, sending SIGKILL.")
|
| 404 |
-
migration_state["process"].kill() # Send SIGKILL if necessary
|
| 405 |
-
migration_state["process"].wait() # Wait for kill
|
| 406 |
-
|
| 407 |
operation = migration_state["operation"]
|
| 408 |
-
log_message(f"
|
| 409 |
-
|
| 410 |
-
#
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
except Exception as e:
|
| 416 |
log_message(f"Error stopping process: {str(e)}", "error")
|
| 417 |
-
#
|
| 418 |
migration_state["process"] = None
|
| 419 |
migration_state["running"] = False
|
|
|
|
| 420 |
return False
|
| 421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
|
| 423 |
@app.get("/", response_class=HTMLResponse)
|
| 424 |
async def home(request: Request):
|
|
@@ -429,7 +508,7 @@ async def home(request: Request):
|
|
| 429 |
with open("templates/index.html", "w") as f:
|
| 430 |
f.write("Placeholder - HTML will be generated") # Basic placeholder
|
| 431 |
|
| 432 |
-
# The actual HTML content
|
| 433 |
html_content = """<!DOCTYPE html>
|
| 434 |
<html lang="en">
|
| 435 |
<head>
|
|
@@ -615,6 +694,10 @@ async def home(request: Request):
|
|
| 615 |
background-color: rgba(239, 68, 68, 0.2);
|
| 616 |
color: var(--color-danger);
|
| 617 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 618 |
.pulse-dot {
|
| 619 |
display: inline-block;
|
| 620 |
width: 8px;
|
|
@@ -1013,6 +1096,8 @@ async def home(request: Request):
|
|
| 1013 |
margin-right: 0.5rem;
|
| 1014 |
flex-shrink: 0;
|
| 1015 |
font-weight: bold;
|
|
|
|
|
|
|
| 1016 |
}
|
| 1017 |
.terminal-command {
|
| 1018 |
color: #fff;
|
|
@@ -1022,25 +1107,25 @@ async def home(request: Request):
|
|
| 1022 |
.terminal-output {
|
| 1023 |
color: #aaa;
|
| 1024 |
white-space: pre-wrap;
|
| 1025 |
-
padding-left: 1rem;
|
| 1026 |
word-break: break-word;
|
| 1027 |
}
|
| 1028 |
.terminal-error {
|
| 1029 |
color: var(--color-danger);
|
| 1030 |
white-space: pre-wrap;
|
| 1031 |
-
padding-left: 1rem;
|
| 1032 |
word-break: break-word;
|
| 1033 |
}
|
| 1034 |
.terminal-success {
|
| 1035 |
color: var(--color-success);
|
| 1036 |
white-space: pre-wrap;
|
| 1037 |
-
padding-left: 1rem;
|
| 1038 |
word-break: break-word;
|
| 1039 |
}
|
| 1040 |
.terminal-warning {
|
| 1041 |
color: var(--color-warning);
|
| 1042 |
white-space: pre-wrap;
|
| 1043 |
-
padding-left: 1rem;
|
| 1044 |
word-break: break-word;
|
| 1045 |
}
|
| 1046 |
.terminal-body::-webkit-scrollbar {
|
|
@@ -1922,6 +2007,10 @@ async def home(request: Request):
|
|
| 1922 |
background-color: rgba(239, 68, 68, 0.1);
|
| 1923 |
color: var(--color-danger);
|
| 1924 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1925 |
</style>
|
| 1926 |
</head>
|
| 1927 |
<body>
|
|
@@ -2134,7 +2223,7 @@ async def home(request: Request):
|
|
| 2134 |
<div class="file-size-value" id="current-size">0 MB</div>
|
| 2135 |
<div class="file-size-subtitle" id="growth-rate">0 MB/s</div>
|
| 2136 |
</div>
|
| 2137 |
-
<div class="badge" id="dump-status">In Progress</div>
|
| 2138 |
</div>
|
| 2139 |
<div class="file-size-chart">
|
| 2140 |
<canvas id="size-chart"></canvas>
|
|
@@ -2164,7 +2253,7 @@ async def home(request: Request):
|
|
| 2164 |
</div>
|
| 2165 |
</div>
|
| 2166 |
<div class="command-visualization" id="dump-command-preview">
|
| 2167 |
-
<div class="command-part command-keyword">pg_dump</div> <div class="command-part command-string">postgres://user:***@hostname:5432/database</div> <div class="command-part command-flag">-Fc</div> <div class="command-part command-flag">-v</div> <div class="command-part command-flag">-f</div> <div class="command-part command-string">timescale_backup.dump</div>
|
| 2168 |
</div>
|
| 2169 |
</div>
|
| 2170 |
</div>
|
|
@@ -2275,7 +2364,7 @@ async def home(request: Request):
|
|
| 2275 |
</div>
|
| 2276 |
</div>
|
| 2277 |
<div class="command-visualization" id="restore-command-preview">
|
| 2278 |
-
<div class="command-part command-keyword">pg_restore</div> <div class="command-part command-flag">-d</div> <div class="command-part command-string">postgres://user:***@hostname:5432/database</div> <div class="command-part command-flag">-v</div> <div class="command-part command-option">--no-owner</div> <div class="command-part command-option">--single-transaction</div> <div class="command-part command-string">timescale_backup.dump</div>
|
| 2279 |
</div>
|
| 2280 |
</div>
|
| 2281 |
</div>
|
|
@@ -2354,13 +2443,13 @@ async def home(request: Request):
|
|
| 2354 |
<p>The restore operation uses <code>pg_restore</code> to import your backup into the target database. It includes TimescaleDB-specific pre and post-restore functions to ensure data integrity.</p>
|
| 2355 |
<h4 class="mt-4 mb-3">Commands Used</h4>
|
| 2356 |
<div class="command-visualization">
|
| 2357 |
-
<div class="command-part command-keyword">pg_dump</div> <div class="command-part command-string">"postgres://user:password@source-host:5432/source_db"</div> <div class="command-part command-flag">-Fc</div> <div class="command-part command-flag">-v</div> <div class="command-part command-flag">-f</div> <div class="command-part command-string"
|
| 2358 |
</div>
|
| 2359 |
<div class="command-visualization mt-3">
|
| 2360 |
<div class="command-part command-keyword">psql</div> <div class="command-part command-string">"postgres://user:password@target-host:5432/target_db"</div> <div class="command-part command-flag">-c</div> <div class="command-part command-string">"SELECT timescaledb_pre_restore();"</div>
|
| 2361 |
</div>
|
| 2362 |
<div class="command-visualization mt-3">
|
| 2363 |
-
<div class="command-part command-keyword">pg_restore</div> <div class="command-part command-flag">-d</div> <div class="command-part command-string">"postgres://user:password@target-host:5432/target_db"</div> <div class="command-part command-flag">-v</div> <div class="command-part command-option">--no-owner</div> <div class="command-part command-string"
|
| 2364 |
</div>
|
| 2365 |
<div class="command-visualization mt-3">
|
| 2366 |
<div class="command-part command-keyword">psql</div> <div class="command-part command-string">"postgres://user:password@target-host:5432/target_db"</div> <div class="command-part command-flag">-c</div> <div class="command-part command-string">"SELECT timescaledb_post_restore(); ANALYZE;"</div>
|
|
@@ -2395,6 +2484,10 @@ async def home(request: Request):
|
|
| 2395 |
<!-- Scripts -->
|
| 2396 |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 2397 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2398 |
document.addEventListener('DOMContentLoaded', function() {
|
| 2399 |
// DOM Elements
|
| 2400 |
const tabs = document.querySelectorAll('.tab');
|
|
@@ -2790,7 +2883,13 @@ async def home(request: Request):
|
|
| 2790 |
}
|
| 2791 |
// Update elapsed time
|
| 2792 |
function startElapsedTimeCounter() {
|
| 2793 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2794 |
if (elapsedTimeInterval) {
|
| 2795 |
clearInterval(elapsedTimeInterval);
|
| 2796 |
}
|
|
@@ -2810,9 +2909,9 @@ async def home(request: Request):
|
|
| 2810 |
elapsedTimeInterval = null;
|
| 2811 |
}
|
| 2812 |
// Final update based on end_time - start_time if available
|
| 2813 |
-
if(migration_state.start_time && migration_state.end_time) {
|
| 2814 |
const elapsed = Math.floor(migration_state.end_time - migration_state.start_time);
|
| 2815 |
-
const formattedTime = formatDuration(elapsed);
|
| 2816 |
if (migration_state.operation === 'dump') {
|
| 2817 |
elapsedTimeElement.textContent = formattedTime;
|
| 2818 |
} else if (migration_state.operation === 'restore') {
|
|
@@ -2832,7 +2931,9 @@ async def home(request: Request):
|
|
| 2832 |
data.dumps.forEach(dump => {
|
| 2833 |
const option = document.createElement('option');
|
| 2834 |
option.value = dump.path;
|
| 2835 |
-
|
|
|
|
|
|
|
| 2836 |
serverBackupFile.appendChild(option);
|
| 2837 |
});
|
| 2838 |
} else {
|
|
@@ -2917,6 +3018,9 @@ async def home(request: Request):
|
|
| 2917 |
|
| 2918 |
// Function to handle starting an operation (dump or restore)
|
| 2919 |
async function handleStartOperation(operationType) {
|
|
|
|
|
|
|
|
|
|
| 2920 |
if (migration_state && migration_state.running) {
|
| 2921 |
confirmModalBody.innerHTML = `A migration operation (${migration_state.operation}) is already in progress. Stopping it will lose all progress. Are you sure you want to start a new ${operationType}?`;
|
| 2922 |
confirmAction = `start-${operationType}`;
|
|
@@ -3019,7 +3123,8 @@ async def home(request: Request):
|
|
| 3019 |
}
|
| 3020 |
// Stop dump
|
| 3021 |
stopDumpBtn.addEventListener('click', () => {
|
| 3022 |
-
|
|
|
|
| 3023 |
confirmAction = 'stop-dump';
|
| 3024 |
confirmModal.classList.add('show');
|
| 3025 |
});
|
|
@@ -3034,21 +3139,10 @@ async def home(request: Request):
|
|
| 3034 |
method: 'POST'
|
| 3035 |
});
|
| 3036 |
const data = await response.json();
|
|
|
|
| 3037 |
if (data.success) {
|
| 3038 |
-
showToast('warning', '
|
| 3039 |
-
//
|
| 3040 |
-
stopStatusUpdates();
|
| 3041 |
-
stopElapsedTimeCounter();
|
| 3042 |
-
// Update UI after stopping updates
|
| 3043 |
-
statusBadge.className = 'status-badge warning';
|
| 3044 |
-
statusBadge.textContent = 'Stopped';
|
| 3045 |
-
dumpStatusElement.textContent = 'Stopped';
|
| 3046 |
-
dumpStatusElement.className = 'badge warning';
|
| 3047 |
-
// Enable start button
|
| 3048 |
-
startDumpBtn.disabled = false;
|
| 3049 |
-
startDumpBtn.innerHTML = '<i class="fas fa-play"></i> Start Dump';
|
| 3050 |
-
// Set terminal output
|
| 3051 |
-
addTerminalLine('Dump process stopped by user', 'warning');
|
| 3052 |
} else {
|
| 3053 |
showToast('error', 'Failed to Stop', data.message || 'Could not stop the process.');
|
| 3054 |
stopDumpBtn.disabled = false; // Re-enable if stop failed
|
|
@@ -3057,7 +3151,7 @@ async def home(request: Request):
|
|
| 3057 |
}
|
| 3058 |
} catch (error) {
|
| 3059 |
console.error('Stop error:', error);
|
| 3060 |
-
showToast('error', 'Error', 'Failed to stop
|
| 3061 |
stopDumpBtn.disabled = false;
|
| 3062 |
stopDumpBtn.innerHTML = '<i class="fas fa-stop"></i> Stop';
|
| 3063 |
startDumpBtn.disabled = false;
|
|
@@ -3157,7 +3251,8 @@ async def home(request: Request):
|
|
| 3157 |
}
|
| 3158 |
// Stop restore
|
| 3159 |
stopRestoreBtn.addEventListener('click', () => {
|
| 3160 |
-
|
|
|
|
| 3161 |
confirmAction = 'stop-restore';
|
| 3162 |
confirmModal.classList.add('show');
|
| 3163 |
});
|
|
@@ -3172,22 +3267,10 @@ async def home(request: Request):
|
|
| 3172 |
method: 'POST'
|
| 3173 |
});
|
| 3174 |
const data = await response.json();
|
|
|
|
| 3175 |
if (data.success) {
|
| 3176 |
-
showToast('warning', '
|
| 3177 |
-
|
| 3178 |
-
stopStatusUpdates();
|
| 3179 |
-
stopElapsedTimeCounter();
|
| 3180 |
-
// Update UI after stopping updates
|
| 3181 |
-
statusBadge.className = 'status-badge warning';
|
| 3182 |
-
statusBadge.textContent = 'Stopped';
|
| 3183 |
-
restoreStatusElement.textContent = 'Stopped';
|
| 3184 |
-
restoreSubstatusElement.textContent = 'Process was terminated by user';
|
| 3185 |
-
restoreProgressBar.classList.remove('animated');
|
| 3186 |
-
// Enable start button
|
| 3187 |
-
startRestoreBtn.disabled = false;
|
| 3188 |
-
startRestoreBtn.innerHTML = '<i class="fas fa-play"></i> Start Restore';
|
| 3189 |
-
// Set terminal output
|
| 3190 |
-
addTerminalLine('Restore process stopped by user', 'warning');
|
| 3191 |
} else {
|
| 3192 |
showToast('error', 'Failed to Stop', data.message || 'Could not stop the process.');
|
| 3193 |
stopRestoreBtn.disabled = false; // Re-enable if stop failed
|
|
@@ -3196,7 +3279,7 @@ async def home(request: Request):
|
|
| 3196 |
}
|
| 3197 |
} catch (error) {
|
| 3198 |
console.error('Stop error:', error);
|
| 3199 |
-
showToast('error', 'Error', 'Failed to stop
|
| 3200 |
stopRestoreBtn.disabled = false;
|
| 3201 |
stopRestoreBtn.innerHTML = '<i class="fas fa-stop"></i> Stop';
|
| 3202 |
startRestoreBtn.disabled = false;
|
|
@@ -3301,7 +3384,8 @@ async def home(request: Request):
|
|
| 3301 |
if (icon) {
|
| 3302 |
line.innerHTML = `<div class="terminal-prompt">${icon}</div> <div class="${className}">${sanitizedText}</div>`;
|
| 3303 |
} else {
|
| 3304 |
-
|
|
|
|
| 3305 |
}
|
| 3306 |
|
| 3307 |
terminalOutput.appendChild(line);
|
|
@@ -3310,6 +3394,11 @@ async def home(request: Request):
|
|
| 3310 |
}
|
| 3311 |
// Add log entry
|
| 3312 |
function addLogEntry(log) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3313 |
const logEntry = document.createElement('div');
|
| 3314 |
logEntry.className = 'log-entry';
|
| 3315 |
logEntry.setAttribute('data-level', log.level);
|
|
@@ -3339,7 +3428,11 @@ async def home(request: Request):
|
|
| 3339 |
if (log.command) {
|
| 3340 |
addTerminalLine(log.command, 'command');
|
| 3341 |
}
|
| 3342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3343 |
}
|
| 3344 |
// Check for new logs
|
| 3345 |
async function checkForNewLogs() {
|
|
@@ -3391,6 +3484,7 @@ async def home(request: Request):
|
|
| 3391 |
return;
|
| 3392 |
}
|
| 3393 |
const data = await response.json();
|
|
|
|
| 3394 |
migration_state = data; // Update local state copy
|
| 3395 |
|
| 3396 |
// Check for new logs first
|
|
@@ -3414,6 +3508,9 @@ async def home(request: Request):
|
|
| 3414 |
stopDumpBtn.disabled = false;
|
| 3415 |
startRestoreBtn.disabled = true; // Disable other operation
|
| 3416 |
stopRestoreBtn.disabled = true;
|
|
|
|
|
|
|
|
|
|
| 3417 |
} else if (data.operation === 'restore') {
|
| 3418 |
updateRestoreProgress(data);
|
| 3419 |
// Ensure buttons reflect running state
|
|
@@ -3421,18 +3518,31 @@ async def home(request: Request):
|
|
| 3421 |
stopRestoreBtn.disabled = false;
|
| 3422 |
startDumpBtn.disabled = true; // Disable other operation
|
| 3423 |
stopDumpBtn.disabled = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3424 |
}
|
| 3425 |
-
} else {
|
| 3426 |
-
// Process finished or idle
|
| 3427 |
-
stopStatusUpdates(); // Stop polling if not running
|
| 3428 |
-
stopElapsedTimeCounter(); // Stop timer
|
| 3429 |
|
| 3430 |
// Check if dump just completed
|
| 3431 |
-
if (data.dump_completed &&
|
| 3432 |
statusBadge.className = 'status-badge success';
|
| 3433 |
statusBadge.textContent = 'Dump Complete';
|
| 3434 |
dumpStatusElement.textContent = 'Completed';
|
| 3435 |
dumpStatusElement.className = 'badge success';
|
|
|
|
|
|
|
| 3436 |
// Enable download button
|
| 3437 |
if (data.dump_file) {
|
| 3438 |
const downloadPath = `/downloads/${data.dump_file.split(/[\\/]/).pop()}`;
|
|
@@ -3452,7 +3562,7 @@ async def home(request: Request):
|
|
| 3452 |
loadServerBackups();
|
| 3453 |
}
|
| 3454 |
// Check if restore just completed
|
| 3455 |
-
else if (data.restore_completed &&
|
| 3456 |
statusBadge.className = 'status-badge success';
|
| 3457 |
statusBadge.textContent = 'Restore Complete';
|
| 3458 |
restoreStatusElement.textContent = 'Completed';
|
|
@@ -3469,8 +3579,8 @@ async def home(request: Request):
|
|
| 3469 |
startDumpBtn.disabled = false; // Re-enable dump
|
| 3470 |
}
|
| 3471 |
// Handle cases where process stopped unexpectedly or was stopped manually
|
| 3472 |
-
else if (
|
| 3473 |
-
const stoppedOperation =
|
| 3474 |
statusBadge.className = 'status-badge warning';
|
| 3475 |
statusBadge.textContent = `${stoppedOperation.charAt(0).toUpperCase() + stoppedOperation.slice(1)} Stopped`;
|
| 3476 |
|
|
@@ -3492,7 +3602,7 @@ async def home(request: Request):
|
|
| 3492 |
}
|
| 3493 |
}
|
| 3494 |
// Otherwise, we are truly idle
|
| 3495 |
-
else {
|
| 3496 |
statusBadge.className = 'status-badge idle';
|
| 3497 |
statusBadge.textContent = 'Idle';
|
| 3498 |
startDumpBtn.disabled = false;
|
|
@@ -3550,7 +3660,6 @@ async def home(request: Request):
|
|
| 3550 |
}
|
| 3551 |
// Estimate progress based on tables completed (if total is known, otherwise just show activity)
|
| 3552 |
// This is a rough estimate as table sizes vary greatly.
|
| 3553 |
-
// A better approach would involve pg_restore's verbose output parsing if possible.
|
| 3554 |
if (data.progress.tables_completed > 0) {
|
| 3555 |
// Simple visual indication of progress, not accurate percentage
|
| 3556 |
const pseudoPercent = Math.min(99, Math.max(5, (data.progress.tables_completed % 20) * 5)); // Cycle 5-99% based on table count
|
|
@@ -3583,11 +3692,11 @@ async def home(request: Request):
|
|
| 3583 |
// Load server backups
|
| 3584 |
await loadServerBackups();
|
| 3585 |
// Initial status check
|
| 3586 |
-
await updateStatus();
|
| 3587 |
// Add initial log check
|
| 3588 |
await checkForNewLogs();
|
| 3589 |
|
| 3590 |
-
// If a process was running when the page loaded, start updates
|
| 3591 |
if (migration_state && migration_state.running) {
|
| 3592 |
startStatusUpdates();
|
| 3593 |
startElapsedTimeCounter(); // Restart timer based on backend start_time
|
|
@@ -3597,6 +3706,8 @@ async def home(request: Request):
|
|
| 3597 |
startDumpBtn.disabled = true;
|
| 3598 |
startRestoreBtn.disabled = true;
|
| 3599 |
if (migration_state.dump_file) dumpFilePathElement.textContent = migration_state.dump_file;
|
|
|
|
|
|
|
| 3600 |
} else if (migration_state.operation === 'restore') {
|
| 3601 |
restoreProgressSection.classList.remove('hidden');
|
| 3602 |
stopRestoreBtn.disabled = false;
|
|
@@ -3610,28 +3721,40 @@ async def home(request: Request):
|
|
| 3610 |
|
| 3611 |
// Restore button click (from dump section)
|
| 3612 |
gotoRestoreFromDumpBtn.addEventListener('click', () => {
|
| 3613 |
-
|
| 3614 |
-
|
| 3615 |
-
|
| 3616 |
-
|
| 3617 |
-
|
| 3618 |
-
|
| 3619 |
-
|
| 3620 |
-
|
| 3621 |
-
|
| 3622 |
-
|
|
|
|
|
|
|
| 3623 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3624 |
}
|
| 3625 |
-
|
| 3626 |
-
|
| 3627 |
-
|
| 3628 |
-
|
| 3629 |
-
// Switch to restore tab
|
| 3630 |
-
document.querySelector('.tab[data-tab="restore"]').click();
|
| 3631 |
-
updateRestoreCommandPreview(); // Update preview with selected file
|
| 3632 |
-
} else {
|
| 3633 |
-
showToast('error', 'Error', 'Cannot determine which backup to restore.');
|
| 3634 |
-
}
|
| 3635 |
});
|
| 3636 |
});
|
| 3637 |
</script>
|
|
@@ -4020,20 +4143,24 @@ async def stop_process_endpoint():
|
|
| 4020 |
"message": "Process stop initiated successfully" # Changed message slightly
|
| 4021 |
})
|
| 4022 |
else:
|
| 4023 |
-
# Check if it wasn't running in the first place
|
| 4024 |
with migration_lock:
|
| 4025 |
is_running = migration_state["running"]
|
| 4026 |
-
|
|
|
|
|
|
|
| 4027 |
return JSONResponse(content={
|
| 4028 |
"success": False, # Technically not an error, but no action taken
|
| 4029 |
"message": "No process was running to stop"
|
| 4030 |
})
|
| 4031 |
-
else: # Stop was called, but failed internally
|
|
|
|
|
|
|
| 4032 |
return JSONResponse(
|
| 4033 |
-
status_code=
|
| 4034 |
content={
|
| 4035 |
-
"success": False,
|
| 4036 |
-
"message": "
|
| 4037 |
})
|
| 4038 |
except Exception as e:
|
| 4039 |
log_message(f"Failed to stop process via endpoint: {str(e)}", "error")
|
|
@@ -4144,5 +4271,6 @@ async def download_file(file_name: str):
|
|
| 4144 |
if __name__ == "__main__":
|
| 4145 |
import uvicorn
|
| 4146 |
# Use reload=True for development, but turn off for production
|
|
|
|
| 4147 |
uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=True)
|
| 4148 |
# For production: uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 15 |
from pathlib import Path
|
| 16 |
import psycopg2
|
| 17 |
import logging
|
| 18 |
+
# Add these imports
|
| 19 |
+
import signal
|
| 20 |
+
# import os # os is already imported
|
| 21 |
+
# import time # time is already imported
|
| 22 |
|
| 23 |
# Setup logging
|
| 24 |
logging.basicConfig(
|
|
|
|
| 176 |
migration_state["dump_completed"] = False
|
| 177 |
migration_state["previous_size"] = 0 # Reset previous size
|
| 178 |
|
| 179 |
+
# Use preexec_fn=os.setsid to create a new process group
|
| 180 |
+
# This is necessary for the killpg logic in stop_current_process
|
| 181 |
+
# Note: preexec_fn is Unix-specific. This won't work directly on Windows.
|
| 182 |
+
preexec_fn_to_use = None
|
| 183 |
+
if hasattr(os, 'setsid'):
|
| 184 |
+
preexec_fn_to_use = os.setsid
|
| 185 |
+
|
| 186 |
process = subprocess.Popen(
|
| 187 |
cmd,
|
| 188 |
stdout=subprocess.PIPE,
|
|
|
|
| 190 |
env=env,
|
| 191 |
text=True,
|
| 192 |
bufsize=1, # Line buffering
|
| 193 |
+
universal_newlines=True,
|
| 194 |
+
preexec_fn=preexec_fn_to_use # Create a new process group
|
| 195 |
)
|
| 196 |
|
| 197 |
with migration_lock:
|
|
|
|
| 307 |
migration_state["restore_completed"] = False
|
| 308 |
migration_state["progress"]["tables_completed"] = 0 # Reset counter
|
| 309 |
|
| 310 |
+
# Use preexec_fn=os.setsid to create a new process group
|
| 311 |
+
preexec_fn_to_use = None
|
| 312 |
+
if hasattr(os, 'setsid'):
|
| 313 |
+
preexec_fn_to_use = os.setsid
|
| 314 |
+
|
| 315 |
process = subprocess.Popen(
|
| 316 |
cmd,
|
| 317 |
stdout=subprocess.PIPE,
|
|
|
|
| 319 |
env=env,
|
| 320 |
text=True,
|
| 321 |
bufsize=1, # Line buffering
|
| 322 |
+
universal_newlines=True,
|
| 323 |
+
preexec_fn=preexec_fn_to_use # Create a new process group
|
| 324 |
)
|
| 325 |
|
| 326 |
with migration_lock:
|
|
|
|
| 406 |
migration_state["process"] = None
|
| 407 |
return False
|
| 408 |
|
| 409 |
+
# Replace the old stop_current_process with the new one
|
| 410 |
def stop_current_process():
|
| 411 |
+
"""Stop the current process with improved forceful termination"""
|
| 412 |
with migration_lock:
|
| 413 |
if migration_state["process"] and migration_state["running"]:
|
| 414 |
try:
|
| 415 |
+
process = migration_state["process"]
|
| 416 |
+
pid = process.pid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
operation = migration_state["operation"]
|
| 418 |
+
log_message(f"Attempting to stop {operation} process (PID: {pid})...", "warning")
|
| 419 |
+
|
| 420 |
+
# Check if process is already terminated before trying to stop
|
| 421 |
+
if process.poll() is not None:
|
| 422 |
+
log_message(f"{operation.capitalize()} process (PID: {pid}) already terminated.", "info")
|
| 423 |
+
migration_state["process"] = None
|
| 424 |
+
migration_state["running"] = False
|
| 425 |
+
return True
|
| 426 |
+
|
| 427 |
+
# First try graceful termination (SIGTERM)
|
| 428 |
+
process.terminate()
|
| 429 |
+
# Wait up to 3 seconds for graceful termination
|
| 430 |
+
for _ in range(30): # 3 seconds with 0.1s checks
|
| 431 |
+
if process.poll() is not None: # Process has terminated
|
| 432 |
+
log_message(f"{operation.capitalize()} process (PID: {pid}) terminated gracefully (SIGTERM)", "warning")
|
| 433 |
+
break
|
| 434 |
+
time.sleep(0.1)
|
| 435 |
+
else: # Loop finished without break, process still running
|
| 436 |
+
# If still running, force kill with SIGKILL
|
| 437 |
+
if process.poll() is None:
|
| 438 |
+
log_message(f"Process (PID: {pid}) not responding to graceful termination, forcing kill (SIGKILL)...", "warning")
|
| 439 |
+
# Try to kill process group (more thorough) - Unix only
|
| 440 |
+
killed_pg = False
|
| 441 |
+
if hasattr(os, 'killpg') and hasattr(os, 'getpgid'):
|
| 442 |
+
try:
|
| 443 |
+
# On Unix systems, negative PID means kill process group
|
| 444 |
+
os.killpg(os.getpgid(pid), signal.SIGKILL)
|
| 445 |
+
killed_pg = True
|
| 446 |
+
log_message(f"Sent SIGKILL to process group of PID {pid}", "warning")
|
| 447 |
+
except ProcessLookupError:
|
| 448 |
+
log_message(f"Process group for PID {pid} not found (already terminated?).", "info")
|
| 449 |
+
# Process likely died between poll and killpg, proceed as if killed
|
| 450 |
+
killed_pg = True # Treat as success for logic below
|
| 451 |
+
except Exception as kill_err:
|
| 452 |
+
log_message(f"Error killing process group for PID {pid}: {kill_err}. Falling back to direct kill.", "error")
|
| 453 |
+
# Fallback to direct kill if process group kill fails
|
| 454 |
+
process.kill()
|
| 455 |
+
log_message(f"Sent SIGKILL directly to PID {pid}", "warning")
|
| 456 |
+
else: # Not on Unix or functions unavailable
|
| 457 |
+
process.kill()
|
| 458 |
+
log_message(f"Sent SIGKILL directly to PID {pid} (killpg not available)", "warning")
|
| 459 |
+
|
| 460 |
+
# Wait a bit for kill to take effect
|
| 461 |
+
time.sleep(0.5)
|
| 462 |
+
process.poll() # Update process status after kill attempt
|
| 463 |
+
|
| 464 |
+
# Final check
|
| 465 |
+
if process.poll() is None:
|
| 466 |
+
log_message(f"Warning: Process (PID: {pid}) may not have terminated successfully after SIGKILL", "error")
|
| 467 |
+
# Even if termination is uncertain, update state to reflect stop attempt
|
| 468 |
+
migration_state["process"] = None
|
| 469 |
+
migration_state["running"] = False
|
| 470 |
+
migration_state["end_time"] = time.time()
|
| 471 |
+
return False # Indicate potential failure
|
| 472 |
+
else:
|
| 473 |
+
log_message(f"Database {operation} operation (PID: {pid}) stopped", "warning")
|
| 474 |
+
migration_state["process"] = None
|
| 475 |
+
migration_state["running"] = False
|
| 476 |
+
migration_state["end_time"] = time.time()
|
| 477 |
+
return True
|
| 478 |
+
|
| 479 |
+
except ProcessLookupError:
|
| 480 |
+
# This can happen if the process terminated between the initial check and trying to kill it
|
| 481 |
+
log_message(f"Process (PID: {pid}) already terminated before stop action completed.", "info")
|
| 482 |
+
migration_state["process"] = None
|
| 483 |
+
migration_state["running"] = False
|
| 484 |
+
migration_state["end_time"] = time.time()
|
| 485 |
+
return True
|
| 486 |
except Exception as e:
|
| 487 |
log_message(f"Error stopping process: {str(e)}", "error")
|
| 488 |
+
# Force state update even on error
|
| 489 |
migration_state["process"] = None
|
| 490 |
migration_state["running"] = False
|
| 491 |
+
migration_state["end_time"] = time.time()
|
| 492 |
return False
|
| 493 |
+
else:
|
| 494 |
+
# No process was running or associated with the state
|
| 495 |
+
log_message("Stop command received, but no process found in current state.", "info")
|
| 496 |
+
# Ensure state reflects not running if it wasn't already
|
| 497 |
+
if migration_state["running"]:
|
| 498 |
+
migration_state["running"] = False
|
| 499 |
+
migration_state["process"] = None
|
| 500 |
+
return False # Indicate no action was needed/taken on a process
|
| 501 |
|
| 502 |
@app.get("/", response_class=HTMLResponse)
|
| 503 |
async def home(request: Request):
|
|
|
|
| 508 |
with open("templates/index.html", "w") as f:
|
| 509 |
f.write("Placeholder - HTML will be generated") # Basic placeholder
|
| 510 |
|
| 511 |
+
# The actual HTML content (NOTE: Frontend changes mentioned in prompt are NOT applied here, only backend)
|
| 512 |
html_content = """<!DOCTYPE html>
|
| 513 |
<html lang="en">
|
| 514 |
<head>
|
|
|
|
| 694 |
background-color: rgba(239, 68, 68, 0.2);
|
| 695 |
color: var(--color-danger);
|
| 696 |
}
|
| 697 |
+
.status-badge.success { /* Added for completed state */
|
| 698 |
+
background-color: rgba(16, 185, 129, 0.2);
|
| 699 |
+
color: var(--color-success);
|
| 700 |
+
}
|
| 701 |
.pulse-dot {
|
| 702 |
display: inline-block;
|
| 703 |
width: 8px;
|
|
|
|
| 1096 |
margin-right: 0.5rem;
|
| 1097 |
flex-shrink: 0;
|
| 1098 |
font-weight: bold;
|
| 1099 |
+
min-width: 1em; /* Ensure space for icon */
|
| 1100 |
+
text-align: center;
|
| 1101 |
}
|
| 1102 |
.terminal-command {
|
| 1103 |
color: #fff;
|
|
|
|
| 1107 |
.terminal-output {
|
| 1108 |
color: #aaa;
|
| 1109 |
white-space: pre-wrap;
|
| 1110 |
+
/* padding-left: 1rem; */ /* Removed padding, rely on prompt */
|
| 1111 |
word-break: break-word;
|
| 1112 |
}
|
| 1113 |
.terminal-error {
|
| 1114 |
color: var(--color-danger);
|
| 1115 |
white-space: pre-wrap;
|
| 1116 |
+
/* padding-left: 1rem; */
|
| 1117 |
word-break: break-word;
|
| 1118 |
}
|
| 1119 |
.terminal-success {
|
| 1120 |
color: var(--color-success);
|
| 1121 |
white-space: pre-wrap;
|
| 1122 |
+
/* padding-left: 1rem; */
|
| 1123 |
word-break: break-word;
|
| 1124 |
}
|
| 1125 |
.terminal-warning {
|
| 1126 |
color: var(--color-warning);
|
| 1127 |
white-space: pre-wrap;
|
| 1128 |
+
/* padding-left: 1rem; */
|
| 1129 |
word-break: break-word;
|
| 1130 |
}
|
| 1131 |
.terminal-body::-webkit-scrollbar {
|
|
|
|
| 2007 |
background-color: rgba(239, 68, 68, 0.1);
|
| 2008 |
color: var(--color-danger);
|
| 2009 |
}
|
| 2010 |
+
.badge.info { /* Added for in-progress */
|
| 2011 |
+
background-color: rgba(59, 130, 246, 0.1);
|
| 2012 |
+
color: var(--color-info);
|
| 2013 |
+
}
|
| 2014 |
</style>
|
| 2015 |
</head>
|
| 2016 |
<body>
|
|
|
|
| 2223 |
<div class="file-size-value" id="current-size">0 MB</div>
|
| 2224 |
<div class="file-size-subtitle" id="growth-rate">0 MB/s</div>
|
| 2225 |
</div>
|
| 2226 |
+
<div class="badge info" id="dump-status">In Progress</div> <!-- Default to info -->
|
| 2227 |
</div>
|
| 2228 |
<div class="file-size-chart">
|
| 2229 |
<canvas id="size-chart"></canvas>
|
|
|
|
| 2253 |
</div>
|
| 2254 |
</div>
|
| 2255 |
<div class="command-visualization" id="dump-command-preview">
|
| 2256 |
+
<div class="command-part command-keyword">pg_dump</div> <div class="command-part command-string">"postgres://user:***@hostname:5432/database"</div> <div class="command-part command-flag">-Fc</div> <div class="command-part command-flag">-v</div> <div class="command-part command-flag">-f</div> <div class="command-part command-string">"timescale_backup.dump"</div>
|
| 2257 |
</div>
|
| 2258 |
</div>
|
| 2259 |
</div>
|
|
|
|
| 2364 |
</div>
|
| 2365 |
</div>
|
| 2366 |
<div class="command-visualization" id="restore-command-preview">
|
| 2367 |
+
<div class="command-part command-keyword">pg_restore</div> <div class="command-part command-flag">-d</div> <div class="command-part command-string">"postgres://user:***@hostname:5432/database"</div> <div class="command-part command-flag">-v</div> <div class="command-part command-option">--no-owner</div> <div class="command-part command-option">--single-transaction</div> <div class="command-part command-string">"timescale_backup.dump"</div>
|
| 2368 |
</div>
|
| 2369 |
</div>
|
| 2370 |
</div>
|
|
|
|
| 2443 |
<p>The restore operation uses <code>pg_restore</code> to import your backup into the target database. It includes TimescaleDB-specific pre and post-restore functions to ensure data integrity.</p>
|
| 2444 |
<h4 class="mt-4 mb-3">Commands Used</h4>
|
| 2445 |
<div class="command-visualization">
|
| 2446 |
+
<div class="command-part command-keyword">pg_dump</div> <div class="command-part command-string">"postgres://user:password@source-host:5432/source_db"</div> <div class="command-part command-flag">-Fc</div> <div class="command-part command-flag">-v</div> <div class="command-part command-flag">-f</div> <div class="command-part command-string">"~/timescale_backup.dump"</div>
|
| 2447 |
</div>
|
| 2448 |
<div class="command-visualization mt-3">
|
| 2449 |
<div class="command-part command-keyword">psql</div> <div class="command-part command-string">"postgres://user:password@target-host:5432/target_db"</div> <div class="command-part command-flag">-c</div> <div class="command-part command-string">"SELECT timescaledb_pre_restore();"</div>
|
| 2450 |
</div>
|
| 2451 |
<div class="command-visualization mt-3">
|
| 2452 |
+
<div class="command-part command-keyword">pg_restore</div> <div class="command-part command-flag">-d</div> <div class="command-part command-string">"postgres://user:password@target-host:5432/target_db"</div> <div class="command-part command-flag">-v</div> <div class="command-part command-option">--no-owner</div> <div class="command-part command-string">"~/timescale_backup.dump"</div>
|
| 2453 |
</div>
|
| 2454 |
<div class="command-visualization mt-3">
|
| 2455 |
<div class="command-part command-keyword">psql</div> <div class="command-part command-string">"postgres://user:password@target-host:5432/target_db"</div> <div class="command-part command-flag">-c</div> <div class="command-part command-string">"SELECT timescaledb_post_restore(); ANALYZE;"</div>
|
|
|
|
| 2484 |
<!-- Scripts -->
|
| 2485 |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 2486 |
<script>
|
| 2487 |
+
// NOTE: This JS is the previous version.
|
| 2488 |
+
// The user prompt included JS changes for the confirmation modal text,
|
| 2489 |
+
// but the request was only to output the modified Python code.
|
| 2490 |
+
// For a fully working system, the JS would also need updating.
|
| 2491 |
document.addEventListener('DOMContentLoaded', function() {
|
| 2492 |
// DOM Elements
|
| 2493 |
const tabs = document.querySelectorAll('.tab');
|
|
|
|
| 2883 |
}
|
| 2884 |
// Update elapsed time
|
| 2885 |
function startElapsedTimeCounter() {
|
| 2886 |
+
// Use start_time from backend state if available and process is running
|
| 2887 |
+
if (migration_state && migration_state.running && migration_state.start_time) {
|
| 2888 |
+
startTime = migration_state.start_time * 1000; // Convert seconds to ms
|
| 2889 |
+
} else {
|
| 2890 |
+
startTime = Date.now();
|
| 2891 |
+
}
|
| 2892 |
+
|
| 2893 |
if (elapsedTimeInterval) {
|
| 2894 |
clearInterval(elapsedTimeInterval);
|
| 2895 |
}
|
|
|
|
| 2909 |
elapsedTimeInterval = null;
|
| 2910 |
}
|
| 2911 |
// Final update based on end_time - start_time if available
|
| 2912 |
+
if(migration_state && migration_state.start_time && migration_state.end_time) {
|
| 2913 |
const elapsed = Math.floor(migration_state.end_time - migration_state.start_time);
|
| 2914 |
+
const formattedTime = formatDuration(elapsed >= 0 ? elapsed : 0);
|
| 2915 |
if (migration_state.operation === 'dump') {
|
| 2916 |
elapsedTimeElement.textContent = formattedTime;
|
| 2917 |
} else if (migration_state.operation === 'restore') {
|
|
|
|
| 2931 |
data.dumps.forEach(dump => {
|
| 2932 |
const option = document.createElement('option');
|
| 2933 |
option.value = dump.path;
|
| 2934 |
+
// Indicate if it's a directory
|
| 2935 |
+
const typeIndicator = dump.is_dir ? ' (Dir)' : '';
|
| 2936 |
+
option.textContent = `${dump.name}${typeIndicator} (${dump.size_mb.toFixed(2)} MB, ${dump.date})`;
|
| 2937 |
serverBackupFile.appendChild(option);
|
| 2938 |
});
|
| 2939 |
} else {
|
|
|
|
| 3018 |
|
| 3019 |
// Function to handle starting an operation (dump or restore)
|
| 3020 |
async function handleStartOperation(operationType) {
|
| 3021 |
+
// Fetch latest status before deciding
|
| 3022 |
+
await updateStatus();
|
| 3023 |
+
|
| 3024 |
if (migration_state && migration_state.running) {
|
| 3025 |
confirmModalBody.innerHTML = `A migration operation (${migration_state.operation}) is already in progress. Stopping it will lose all progress. Are you sure you want to start a new ${operationType}?`;
|
| 3026 |
confirmAction = `start-${operationType}`;
|
|
|
|
| 3123 |
}
|
| 3124 |
// Stop dump
|
| 3125 |
stopDumpBtn.addEventListener('click', () => {
|
| 3126 |
+
// Use the updated text from the prompt
|
| 3127 |
+
confirmModalBody.innerHTML = 'Stopping the dump will terminate the process and you will need to start over. The process may take a few seconds to stop completely. Are you sure you want to stop?';
|
| 3128 |
confirmAction = 'stop-dump';
|
| 3129 |
confirmModal.classList.add('show');
|
| 3130 |
});
|
|
|
|
| 3139 |
method: 'POST'
|
| 3140 |
});
|
| 3141 |
const data = await response.json();
|
| 3142 |
+
// UI updates will be handled by the status poller detecting the change
|
| 3143 |
if (data.success) {
|
| 3144 |
+
showToast('warning', 'Stop Initiated', 'Attempting to stop the dump process...');
|
| 3145 |
+
// The status poller will eventually update the UI state fully
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3146 |
} else {
|
| 3147 |
showToast('error', 'Failed to Stop', data.message || 'Could not stop the process.');
|
| 3148 |
stopDumpBtn.disabled = false; // Re-enable if stop failed
|
|
|
|
| 3151 |
}
|
| 3152 |
} catch (error) {
|
| 3153 |
console.error('Stop error:', error);
|
| 3154 |
+
showToast('error', 'Error', 'Failed to send stop command.');
|
| 3155 |
stopDumpBtn.disabled = false;
|
| 3156 |
stopDumpBtn.innerHTML = '<i class="fas fa-stop"></i> Stop';
|
| 3157 |
startDumpBtn.disabled = false;
|
|
|
|
| 3251 |
}
|
| 3252 |
// Stop restore
|
| 3253 |
stopRestoreBtn.addEventListener('click', () => {
|
| 3254 |
+
// Use the updated text from the prompt
|
| 3255 |
+
confirmModalBody.innerHTML = 'Stopping the restore will terminate the process and might leave the database in an inconsistent state. The process may take a few seconds to stop completely. Are you sure you want to stop?';
|
| 3256 |
confirmAction = 'stop-restore';
|
| 3257 |
confirmModal.classList.add('show');
|
| 3258 |
});
|
|
|
|
| 3267 |
method: 'POST'
|
| 3268 |
});
|
| 3269 |
const data = await response.json();
|
| 3270 |
+
// UI updates will be handled by the status poller detecting the change
|
| 3271 |
if (data.success) {
|
| 3272 |
+
showToast('warning', 'Stop Initiated', 'Attempting to stop the restore process...');
|
| 3273 |
+
// The status poller will eventually update the UI state fully
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3274 |
} else {
|
| 3275 |
showToast('error', 'Failed to Stop', data.message || 'Could not stop the process.');
|
| 3276 |
stopRestoreBtn.disabled = false; // Re-enable if stop failed
|
|
|
|
| 3279 |
}
|
| 3280 |
} catch (error) {
|
| 3281 |
console.error('Stop error:', error);
|
| 3282 |
+
showToast('error', 'Error', 'Failed to send stop command.');
|
| 3283 |
stopRestoreBtn.disabled = false;
|
| 3284 |
stopRestoreBtn.innerHTML = '<i class="fas fa-stop"></i> Stop';
|
| 3285 |
startRestoreBtn.disabled = false;
|
|
|
|
| 3384 |
if (icon) {
|
| 3385 |
line.innerHTML = `<div class="terminal-prompt">${icon}</div> <div class="${className}">${sanitizedText}</div>`;
|
| 3386 |
} else {
|
| 3387 |
+
// For plain output, don't add a prompt div, just the message
|
| 3388 |
+
line.innerHTML = `<div class="${className}" style="padding-left: calc(1em + 0.5rem);">${sanitizedText}</div>`;
|
| 3389 |
}
|
| 3390 |
|
| 3391 |
terminalOutput.appendChild(line);
|
|
|
|
| 3394 |
}
|
| 3395 |
// Add log entry
|
| 3396 |
function addLogEntry(log) {
|
| 3397 |
+
// Avoid adding duplicate logs if status updates overlap slightly
|
| 3398 |
+
if (document.querySelector(`.log-entry[data-id="${log.id}"]`)) {
|
| 3399 |
+
return;
|
| 3400 |
+
}
|
| 3401 |
+
|
| 3402 |
const logEntry = document.createElement('div');
|
| 3403 |
logEntry.className = 'log-entry';
|
| 3404 |
logEntry.setAttribute('data-level', log.level);
|
|
|
|
| 3428 |
if (log.command) {
|
| 3429 |
addTerminalLine(log.command, 'command');
|
| 3430 |
}
|
| 3431 |
+
// Only add non-command messages if they are not just verbose process output (heuristic)
|
| 3432 |
+
// This avoids cluttering the terminal too much with pg_dump/restore verbose lines
|
| 3433 |
+
if (!log.command && (log.level !== 'info' || !log.message.startsWith('pg_'))) {
|
| 3434 |
+
addTerminalLine(log.message, terminalType);
|
| 3435 |
+
}
|
| 3436 |
}
|
| 3437 |
// Check for new logs
|
| 3438 |
async function checkForNewLogs() {
|
|
|
|
| 3484 |
return;
|
| 3485 |
}
|
| 3486 |
const data = await response.json();
|
| 3487 |
+
const previousState = migration_state; // Store previous state for comparison
|
| 3488 |
migration_state = data; // Update local state copy
|
| 3489 |
|
| 3490 |
// Check for new logs first
|
|
|
|
| 3508 |
stopDumpBtn.disabled = false;
|
| 3509 |
startRestoreBtn.disabled = true; // Disable other operation
|
| 3510 |
stopRestoreBtn.disabled = true;
|
| 3511 |
+
if (!dumpProgressSection.classList.contains('hidden')) {
|
| 3512 |
+
dumpProgressSection.classList.remove('hidden'); // Ensure visible
|
| 3513 |
+
}
|
| 3514 |
} else if (data.operation === 'restore') {
|
| 3515 |
updateRestoreProgress(data);
|
| 3516 |
// Ensure buttons reflect running state
|
|
|
|
| 3518 |
stopRestoreBtn.disabled = false;
|
| 3519 |
startDumpBtn.disabled = true; // Disable other operation
|
| 3520 |
stopDumpBtn.disabled = true;
|
| 3521 |
+
if (!restoreProgressSection.classList.contains('hidden')) {
|
| 3522 |
+
restoreProgressSection.classList.remove('hidden'); // Ensure visible
|
| 3523 |
+
}
|
| 3524 |
+
}
|
| 3525 |
+
// If the process just started, start the timer
|
| 3526 |
+
if (!previousState.running && data.running) {
|
| 3527 |
+
startElapsedTimeCounter();
|
| 3528 |
+
startStatusUpdates(); // Ensure updates continue
|
| 3529 |
+
}
|
| 3530 |
+
|
| 3531 |
+
} else { // Not running
|
| 3532 |
+
// If it *was* running previously, stop timers/updates
|
| 3533 |
+
if (previousState.running && !data.running) {
|
| 3534 |
+
stopStatusUpdates();
|
| 3535 |
+
stopElapsedTimeCounter();
|
| 3536 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3537 |
|
| 3538 |
// Check if dump just completed
|
| 3539 |
+
if (data.dump_completed && previousState.running && previousState.operation === 'dump') {
|
| 3540 |
statusBadge.className = 'status-badge success';
|
| 3541 |
statusBadge.textContent = 'Dump Complete';
|
| 3542 |
dumpStatusElement.textContent = 'Completed';
|
| 3543 |
dumpStatusElement.className = 'badge success';
|
| 3544 |
+
// Ensure final size/rate is displayed
|
| 3545 |
+
updateDumpProgress(data);
|
| 3546 |
// Enable download button
|
| 3547 |
if (data.dump_file) {
|
| 3548 |
const downloadPath = `/downloads/${data.dump_file.split(/[\\/]/).pop()}`;
|
|
|
|
| 3562 |
loadServerBackups();
|
| 3563 |
}
|
| 3564 |
// Check if restore just completed
|
| 3565 |
+
else if (data.restore_completed && previousState.running && previousState.operation === 'restore') {
|
| 3566 |
statusBadge.className = 'status-badge success';
|
| 3567 |
statusBadge.textContent = 'Restore Complete';
|
| 3568 |
restoreStatusElement.textContent = 'Completed';
|
|
|
|
| 3579 |
startDumpBtn.disabled = false; // Re-enable dump
|
| 3580 |
}
|
| 3581 |
// Handle cases where process stopped unexpectedly or was stopped manually
|
| 3582 |
+
else if (previousState.running && !data.running) {
|
| 3583 |
+
const stoppedOperation = previousState.operation; // Use previous state's operation
|
| 3584 |
statusBadge.className = 'status-badge warning';
|
| 3585 |
statusBadge.textContent = `${stoppedOperation.charAt(0).toUpperCase() + stoppedOperation.slice(1)} Stopped`;
|
| 3586 |
|
|
|
|
| 3602 |
}
|
| 3603 |
}
|
| 3604 |
// Otherwise, we are truly idle
|
| 3605 |
+
else if (!data.running) {
|
| 3606 |
statusBadge.className = 'status-badge idle';
|
| 3607 |
statusBadge.textContent = 'Idle';
|
| 3608 |
startDumpBtn.disabled = false;
|
|
|
|
| 3660 |
}
|
| 3661 |
// Estimate progress based on tables completed (if total is known, otherwise just show activity)
|
| 3662 |
// This is a rough estimate as table sizes vary greatly.
|
|
|
|
| 3663 |
if (data.progress.tables_completed > 0) {
|
| 3664 |
// Simple visual indication of progress, not accurate percentage
|
| 3665 |
const pseudoPercent = Math.min(99, Math.max(5, (data.progress.tables_completed % 20) * 5)); // Cycle 5-99% based on table count
|
|
|
|
| 3692 |
// Load server backups
|
| 3693 |
await loadServerBackups();
|
| 3694 |
// Initial status check
|
| 3695 |
+
await updateStatus(); // This also updates migration_state
|
| 3696 |
// Add initial log check
|
| 3697 |
await checkForNewLogs();
|
| 3698 |
|
| 3699 |
+
// If a process was running when the page loaded, sync UI and start updates
|
| 3700 |
if (migration_state && migration_state.running) {
|
| 3701 |
startStatusUpdates();
|
| 3702 |
startElapsedTimeCounter(); // Restart timer based on backend start_time
|
|
|
|
| 3706 |
startDumpBtn.disabled = true;
|
| 3707 |
startRestoreBtn.disabled = true;
|
| 3708 |
if (migration_state.dump_file) dumpFilePathElement.textContent = migration_state.dump_file;
|
| 3709 |
+
// Restore chart data if possible (limited history)
|
| 3710 |
+
// This part is complex and might require storing recent points in backend state
|
| 3711 |
} else if (migration_state.operation === 'restore') {
|
| 3712 |
restoreProgressSection.classList.remove('hidden');
|
| 3713 |
stopRestoreBtn.disabled = false;
|
|
|
|
| 3721 |
|
| 3722 |
// Restore button click (from dump section)
|
| 3723 |
gotoRestoreFromDumpBtn.addEventListener('click', () => {
|
| 3724 |
+
// Fetch latest status to get the correct dump file path
|
| 3725 |
+
fetch('/status').then(res => res.json()).then(data => {
|
| 3726 |
+
if (data && data.dump_file) {
|
| 3727 |
+
const dumpPath = data.dump_file;
|
| 3728 |
+
const options = serverBackupFile.options;
|
| 3729 |
+
let found = false;
|
| 3730 |
+
for (let i = 0; i < options.length; i++) {
|
| 3731 |
+
if (options[i].value === dumpPath) {
|
| 3732 |
+
serverBackupFile.selectedIndex = i;
|
| 3733 |
+
found = true;
|
| 3734 |
+
break;
|
| 3735 |
+
}
|
| 3736 |
}
|
| 3737 |
+
if (!found) {
|
| 3738 |
+
showToast('warning', 'Backup Not Found', 'The completed backup file is not in the dropdown list. Refreshing list...');
|
| 3739 |
+
loadServerBackups().then(() => { // Reload and try again
|
| 3740 |
+
for (let i = 0; i < serverBackupFile.options.length; i++) {
|
| 3741 |
+
if (serverBackupFile.options[i].value === dumpPath) {
|
| 3742 |
+
serverBackupFile.selectedIndex = i;
|
| 3743 |
+
break;
|
| 3744 |
+
}
|
| 3745 |
+
}
|
| 3746 |
+
});
|
| 3747 |
+
}
|
| 3748 |
+
// Switch to restore tab
|
| 3749 |
+
document.querySelector('.tab[data-tab="restore"]').click();
|
| 3750 |
+
updateRestoreCommandPreview(); // Update preview with selected file
|
| 3751 |
+
} else {
|
| 3752 |
+
showToast('error', 'Error', 'Cannot determine which backup to restore.');
|
| 3753 |
}
|
| 3754 |
+
}).catch(err => {
|
| 3755 |
+
console.error("Error fetching status for restore button:", err);
|
| 3756 |
+
showToast('error', 'Error', 'Could not get latest status.');
|
| 3757 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3758 |
});
|
| 3759 |
});
|
| 3760 |
</script>
|
|
|
|
| 4143 |
"message": "Process stop initiated successfully" # Changed message slightly
|
| 4144 |
})
|
| 4145 |
else:
|
| 4146 |
+
# Check if it wasn't running in the first place or if stop failed
|
| 4147 |
with migration_lock:
|
| 4148 |
is_running = migration_state["running"]
|
| 4149 |
+
was_process = migration_state["process"] is not None # Check if we *thought* a process existed
|
| 4150 |
+
|
| 4151 |
+
if not is_running and not was_process:
|
| 4152 |
return JSONResponse(content={
|
| 4153 |
"success": False, # Technically not an error, but no action taken
|
| 4154 |
"message": "No process was running to stop"
|
| 4155 |
})
|
| 4156 |
+
else: # Stop was called, but failed internally or process already gone
|
| 4157 |
+
# The stop_current_process function now returns False on failure or if already stopped
|
| 4158 |
+
# Check logs for specific reason
|
| 4159 |
return JSONResponse(
|
| 4160 |
+
status_code=200, # Return 200, but indicate potential issue in message
|
| 4161 |
content={
|
| 4162 |
+
"success": False, # Indicate stop wasn't fully successful *now*
|
| 4163 |
+
"message": "Stop command executed. Check logs for termination status."
|
| 4164 |
})
|
| 4165 |
except Exception as e:
|
| 4166 |
log_message(f"Failed to stop process via endpoint: {str(e)}", "error")
|
|
|
|
| 4271 |
if __name__ == "__main__":
|
| 4272 |
import uvicorn
|
| 4273 |
# Use reload=True for development, but turn off for production
|
| 4274 |
+
# Assuming the file is named main.py for reload to work correctly
|
| 4275 |
uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=True)
|
| 4276 |
# For production: uvicorn.run(app, host="0.0.0.0", port=7860)
|