Change : Backup pick
This commit is contained in:
@@ -43,6 +43,10 @@ app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
|
|||||||
restore_jobs = {}
|
restore_jobs = {}
|
||||||
backup_jobs = {}
|
backup_jobs = {}
|
||||||
|
|
||||||
|
# Whitelist of apps the restore script knows how to selectively restore.
|
||||||
|
# Anything not in this set is silently dropped before it ever reaches a shell string.
|
||||||
|
ALLOWED_RESTORE_APPS = {'frappe', 'odoo', 'nextcloud', 'mautic', 'n8n'}
|
||||||
|
|
||||||
|
|
||||||
def _stream_restore(job_id, cmd):
|
def _stream_restore(job_id, cmd):
|
||||||
restore_jobs[job_id] = {'status': 'running', 'log': [], 'started': time.time()}
|
restore_jobs[job_id] = {'status': 'running', 'log': [], 'started': time.time()}
|
||||||
@@ -732,6 +736,20 @@ def restore_start():
|
|||||||
if not backup_file:
|
if not backup_file:
|
||||||
return jsonify({'error': 'No backup file specified'}), 400
|
return jsonify({'error': 'No backup file specified'}), 400
|
||||||
|
|
||||||
|
# ── Selective app restore ────────────────────────────────────
|
||||||
|
# `apps` is optional. Empty/missing => full restore (all apps).
|
||||||
|
# Anything not in ALLOWED_RESTORE_APPS is dropped before it can
|
||||||
|
# reach a shell string.
|
||||||
|
raw_apps = data.get('apps', [])
|
||||||
|
if isinstance(raw_apps, str):
|
||||||
|
raw_apps = [a.strip() for a in raw_apps.split(',') if a.strip()]
|
||||||
|
selected_apps = sorted({
|
||||||
|
a.strip().lower() for a in raw_apps
|
||||||
|
if isinstance(a, str) and a.strip().lower() in ALLOWED_RESTORE_APPS
|
||||||
|
})
|
||||||
|
apps_flag = f" --apps {','.join(selected_apps)}" if selected_apps else ""
|
||||||
|
apps_label = ','.join(selected_apps) if selected_apps else 'ALL'
|
||||||
|
|
||||||
# ── Resolve backup path by source ────────────────────────────
|
# ── Resolve backup path by source ────────────────────────────
|
||||||
if backup_source == 'cloud':
|
if backup_source == 'cloud':
|
||||||
backup_path = f"/tmp/{backup_file}"
|
backup_path = f"/tmp/{backup_file}"
|
||||||
@@ -794,12 +812,13 @@ def restore_start():
|
|||||||
cmd = (
|
cmd = (
|
||||||
f"set -e && "
|
f"set -e && "
|
||||||
f"echo 'Restoring on this server ({hostname})...' && "
|
f"echo 'Restoring on this server ({hostname})...' && "
|
||||||
|
f"echo 'Apps: {apps_label}' && "
|
||||||
f"mkdir -p {session_dir} && "
|
f"mkdir -p {session_dir} && "
|
||||||
f"echo 'Extracting backup...' && "
|
f"echo 'Extracting backup...' && "
|
||||||
f"tar -xzf {backup_path} -C {session_dir} --strip-components=1 && "
|
f"tar -xzf {backup_path} -C {session_dir} --strip-components=1 && "
|
||||||
f"cp {restore_script_local} {session_dir}/restore-myapps.sh && "
|
f"cp {restore_script_local} {session_dir}/restore-myapps.sh && "
|
||||||
f"chmod +x {session_dir}/restore-myapps.sh && "
|
f"chmod +x {session_dir}/restore-myapps.sh && "
|
||||||
f"cd {session_dir} && bash restore-myapps.sh ; "
|
f"cd {session_dir} && bash restore-myapps.sh{apps_flag} ; "
|
||||||
f"EXIT=$? ; rm -rf {session_dir} ; exit $EXIT"
|
f"EXIT=$? ; rm -rf {session_dir} ; exit $EXIT"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -821,6 +840,7 @@ def restore_start():
|
|||||||
remote_dest = f"/backups/restore-session-{uuid.uuid4().hex[:8]}"
|
remote_dest = f"/backups/restore-session-{uuid.uuid4().hex[:8]}"
|
||||||
cmd = (
|
cmd = (
|
||||||
f"echo 'Connecting to {remote_user}@{remote_ip}:{remote_port}...' && "
|
f"echo 'Connecting to {remote_user}@{remote_ip}:{remote_port}...' && "
|
||||||
|
f"echo 'Apps: {apps_label}' && "
|
||||||
f"{ssh_prefix} {remote_user}@{remote_ip} 'mkdir -p {remote_dest}' && "
|
f"{ssh_prefix} {remote_user}@{remote_ip} 'mkdir -p {remote_dest}' && "
|
||||||
f"echo 'Connected.' && "
|
f"echo 'Connected.' && "
|
||||||
f"echo 'Copying backup archive...' && "
|
f"echo 'Copying backup archive...' && "
|
||||||
@@ -831,7 +851,7 @@ def restore_start():
|
|||||||
f"{ssh_prefix} {remote_user}@{remote_ip} "
|
f"{ssh_prefix} {remote_user}@{remote_ip} "
|
||||||
f"'set -e && cd {remote_dest} && "
|
f"'set -e && cd {remote_dest} && "
|
||||||
f"tar -xzf {backup_file} --strip-components=1 && "
|
f"tar -xzf {backup_file} --strip-components=1 && "
|
||||||
f"chmod +x restore-myapps.sh && bash restore-myapps.sh' ; "
|
f"chmod +x restore-myapps.sh && bash restore-myapps.sh{apps_flag}' ; "
|
||||||
f"EXIT=$? ; "
|
f"EXIT=$? ; "
|
||||||
f"{ssh_prefix} {remote_user}@{remote_ip} 'rm -rf {remote_dest}' 2>/dev/null ; "
|
f"{ssh_prefix} {remote_user}@{remote_ip} 'rm -rf {remote_dest}' 2>/dev/null ; "
|
||||||
f"exit $EXIT"
|
f"exit $EXIT"
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ REMOTE_IP=""
|
|||||||
REMOTE_USER="root"
|
REMOTE_USER="root"
|
||||||
SSH_KEY=""
|
SSH_KEY=""
|
||||||
USE_PASSWORD=false
|
USE_PASSWORD=false
|
||||||
|
APPS_FILTER="all" # "all" or comma-separated: frappe,odoo,nextcloud,mautic,n8n
|
||||||
|
|
||||||
|
VALID_APPS="frappe odoo nextcloud mautic n8n"
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -33,12 +36,37 @@ while [[ $# -gt 0 ]]; do
|
|||||||
USE_PASSWORD=true
|
USE_PASSWORD=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
--apps)
|
||||||
|
APPS_FILTER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Normalize + validate --apps
|
||||||
|
# --------------------------------------------------
|
||||||
|
if [ "$APPS_FILTER" != "all" ]; then
|
||||||
|
APPS_FILTER=$(echo "$APPS_FILTER" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
|
||||||
|
IFS=',' read -ra _APP_CHECK <<< "$APPS_FILTER"
|
||||||
|
for _a in "${_APP_CHECK[@]}"; do
|
||||||
|
if [[ " $VALID_APPS " != *" $_a "* ]]; then
|
||||||
|
echo "❌ Unknown app '$_a' in --apps. Valid options: $VALID_APPS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Returns 0 (true) if $1 should be restored given APPS_FILTER
|
||||||
|
app_selected() {
|
||||||
|
local app="$1"
|
||||||
|
[ "$APPS_FILTER" = "all" ] && return 0
|
||||||
|
[[ ",$APPS_FILTER," == *",${app},"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# Remote mode
|
# Remote mode
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
@@ -74,11 +102,21 @@ if [ "$REMOTE_MODE" = true ]; then
|
|||||||
echo "========================================="
|
echo "========================================="
|
||||||
echo "📡 REMOTE RESTORE MODE"
|
echo "📡 REMOTE RESTORE MODE"
|
||||||
echo " Target: ${REMOTE_USER}@${REMOTE_IP}"
|
echo " Target: ${REMOTE_USER}@${REMOTE_IP}"
|
||||||
|
if [ "$APPS_FILTER" = "all" ]; then
|
||||||
|
echo " Apps: ALL"
|
||||||
|
else
|
||||||
|
echo " Apps: $APPS_FILTER"
|
||||||
|
fi
|
||||||
echo "========================================="
|
echo "========================================="
|
||||||
|
|
||||||
|
REMOTE_ARGS=""
|
||||||
|
if [ "$APPS_FILTER" != "all" ]; then
|
||||||
|
REMOTE_ARGS="--apps $APPS_FILTER"
|
||||||
|
fi
|
||||||
|
|
||||||
$SSH_CMD "mkdir -p $REMOTE_DEST"
|
$SSH_CMD "mkdir -p $REMOTE_DEST"
|
||||||
$SCP_CMD -r "$SCRIPT_DIR/." "${REMOTE_USER}@${REMOTE_IP}:${REMOTE_DEST}/"
|
$SCP_CMD -r "$SCRIPT_DIR/." "${REMOTE_USER}@${REMOTE_IP}:${REMOTE_DEST}/"
|
||||||
$SSH_CMD "chmod +x $REMOTE_DEST/restore-myapps.sh && cd $REMOTE_DEST && bash restore-myapps.sh"
|
$SSH_CMD "chmod +x $REMOTE_DEST/restore-myapps.sh && cd $REMOTE_DEST && bash restore-myapps.sh $REMOTE_ARGS"
|
||||||
$SSH_CMD "rm -rf $REMOTE_DEST"
|
$SSH_CMD "rm -rf $REMOTE_DEST"
|
||||||
|
|
||||||
echo "✅ Remote restore complete on $REMOTE_IP"
|
echo "✅ Remote restore complete on $REMOTE_IP"
|
||||||
@@ -101,6 +139,11 @@ echo "========================================="
|
|||||||
echo "🔄 Smart Restore — LOCAL MODE"
|
echo "🔄 Smart Restore — LOCAL MODE"
|
||||||
echo " Machine IP: $VM_IP"
|
echo " Machine IP: $VM_IP"
|
||||||
echo " Backup dir: $SCRIPT_DIR"
|
echo " Backup dir: $SCRIPT_DIR"
|
||||||
|
if [ "$APPS_FILTER" = "all" ]; then
|
||||||
|
echo " Apps: ALL"
|
||||||
|
else
|
||||||
|
echo " Apps: $APPS_FILTER (forced — ignoring health checks)"
|
||||||
|
fi
|
||||||
echo "========================================="
|
echo "========================================="
|
||||||
|
|
||||||
container_is_healthy() {
|
container_is_healthy() {
|
||||||
@@ -136,14 +179,36 @@ declare -A VOLUME_OWNERS=(
|
|||||||
["odoo-clean_odoo-etc"]="odoo-clean-odoo-1"
|
["odoo-clean_odoo-etc"]="odoo-clean-odoo-1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
declare -A VOLUME_APP=(
|
||||||
|
["frappe-setup_frappe-sites"]="frappe"
|
||||||
|
["frappe-setup_mariadb-data"]="frappe"
|
||||||
|
["nextcloud-setup_nextcloud-data"]="nextcloud"
|
||||||
|
["nextcloud-setup_nextcloud-db-data"]="nextcloud"
|
||||||
|
["mautic-setup_mautic-data"]="mautic"
|
||||||
|
["mautic-setup_mautic-db-data"]="mautic"
|
||||||
|
["n8n-setup_n8n-data"]="n8n"
|
||||||
|
["n8n-setup_n8n-db-data"]="n8n"
|
||||||
|
["odoo-clean_db-data"]="odoo"
|
||||||
|
["odoo-clean_odoo-etc"]="odoo"
|
||||||
|
)
|
||||||
|
|
||||||
if [ -d "$SCRIPT_DIR/volumes" ]; then
|
if [ -d "$SCRIPT_DIR/volumes" ]; then
|
||||||
cd "$SCRIPT_DIR/volumes"
|
cd "$SCRIPT_DIR/volumes"
|
||||||
for backup in *.tar.gz; do
|
for backup in *.tar.gz; do
|
||||||
[ -f "$backup" ] || continue
|
[ -f "$backup" ] || continue
|
||||||
volume=$(basename "$backup" .tar.gz)
|
volume=$(basename "$backup" .tar.gz)
|
||||||
owner="${VOLUME_OWNERS[$volume]:-}"
|
owner="${VOLUME_OWNERS[$volume]:-}"
|
||||||
|
vol_app="${VOLUME_APP[$volume]:-}"
|
||||||
|
|
||||||
if [ -n "$owner" ] && container_is_healthy "$owner"; then
|
if [ -n "$vol_app" ] && ! app_selected "$vol_app"; then
|
||||||
|
echo " ⏭️ $volume — app '$vol_app' not selected, skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Health-based protection only applies on full ("all") runs.
|
||||||
|
# An explicit --apps selection means the user wants this restored
|
||||||
|
# regardless of what the health check currently reports.
|
||||||
|
if [ "$APPS_FILTER" = "all" ] && [ -n "$owner" ] && container_is_healthy "$owner"; then
|
||||||
echo " ⏭️ $volume — container '$owner' is healthy, skipping"
|
echo " ⏭️ $volume — container '$owner' is healthy, skipping"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
@@ -186,25 +251,47 @@ declare -A APP_MAIN_CONTAINER=(
|
|||||||
["n8n"]="n8n-app"
|
["n8n"]="n8n-app"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
declare -A APP_KEY=(
|
||||||
|
["Frappe"]="frappe"
|
||||||
|
["Odoo"]="odoo"
|
||||||
|
["Nextcloud"]="nextcloud"
|
||||||
|
["Mautic"]="mautic"
|
||||||
|
["n8n"]="n8n"
|
||||||
|
)
|
||||||
|
|
||||||
if [ -d "$SCRIPT_DIR/compose-files" ]; then
|
if [ -d "$SCRIPT_DIR/compose-files" ]; then
|
||||||
cd "$SCRIPT_DIR/compose-files"
|
cd "$SCRIPT_DIR/compose-files"
|
||||||
for app in Frappe Odoo Nextcloud Mautic n8n; do
|
for app in Frappe Odoo Nextcloud Mautic n8n; do
|
||||||
|
app_key="${APP_KEY[$app]}"
|
||||||
dir="${APP_DIRS[$app]}"
|
dir="${APP_DIRS[$app]}"
|
||||||
main_ctr="${APP_MAIN_CONTAINER[$app]}"
|
main_ctr="${APP_MAIN_CONTAINER[$app]}"
|
||||||
|
|
||||||
if container_is_healthy "$main_ctr"; then
|
if ! app_selected "$app_key"; then
|
||||||
|
echo " ⏭️ $app — not selected, skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$dir" ]; then
|
||||||
|
echo " ⏭️ $app compose dir '$dir' not found, skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$APPS_FILTER" = "all" ] && container_is_healthy "$main_ctr"; then
|
||||||
echo " ⏭️ $app — already running and healthy, skipping"
|
echo " ⏭️ $app — already running and healthy, skipping"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -d "$dir" ]; then
|
cd "$dir"
|
||||||
echo " 🚀 Starting $app..."
|
if [ "$APPS_FILTER" != "all" ] && container_is_healthy "$main_ctr"; then
|
||||||
cd "$dir"
|
# Already running — up -d alone won't reload the freshly restored
|
||||||
docker-compose up -d 2>&1 | tail -3
|
# volume, so force a recreate.
|
||||||
cd ..
|
echo " 🔁 $app — explicitly selected, recreating to load restored data..."
|
||||||
|
docker-compose down 2>&1 | tail -3
|
||||||
else
|
else
|
||||||
echo " ⏭️ $app compose dir '$dir' not found, skipping"
|
echo " 🚀 Starting $app..."
|
||||||
fi
|
fi
|
||||||
|
docker-compose up -d 2>&1 | tail -3
|
||||||
|
cd ..
|
||||||
done
|
done
|
||||||
cd "$SCRIPT_DIR"
|
cd "$SCRIPT_DIR"
|
||||||
else
|
else
|
||||||
@@ -226,7 +313,9 @@ echo "========================================="
|
|||||||
# ---- NEXTCLOUD ----
|
# ---- NEXTCLOUD ----
|
||||||
echo ""
|
echo ""
|
||||||
echo "📌 Nextcloud — Trusted domains..."
|
echo "📌 Nextcloud — Trusted domains..."
|
||||||
if container_is_healthy nextcloud-app; then
|
if ! app_selected nextcloud; then
|
||||||
|
echo " ⏭️ Nextcloud not selected, skipping"
|
||||||
|
elif [ "$APPS_FILTER" = "all" ] && container_is_healthy nextcloud-app; then
|
||||||
echo " ⏭️ Nextcloud is healthy, skipping fix"
|
echo " ⏭️ Nextcloud is healthy, skipping fix"
|
||||||
else
|
else
|
||||||
if docker ps | grep -q nextcloud-app; then
|
if docker ps | grep -q nextcloud-app; then
|
||||||
@@ -250,7 +339,9 @@ fi
|
|||||||
# ---- MAUTIC ----
|
# ---- MAUTIC ----
|
||||||
echo ""
|
echo ""
|
||||||
echo "📌 Mautic — Config + admin password..."
|
echo "📌 Mautic — Config + admin password..."
|
||||||
if container_is_healthy mautic-app; then
|
if ! app_selected mautic; then
|
||||||
|
echo " ⏭️ Mautic not selected, skipping"
|
||||||
|
elif [ "$APPS_FILTER" = "all" ] && container_is_healthy mautic-app; then
|
||||||
echo " ⏭️ Mautic is healthy, skipping fix"
|
echo " ⏭️ Mautic is healthy, skipping fix"
|
||||||
else
|
else
|
||||||
if docker ps | grep -q mautic-app; then
|
if docker ps | grep -q mautic-app; then
|
||||||
@@ -288,7 +379,9 @@ fi
|
|||||||
# ---- ODOO ----
|
# ---- ODOO ----
|
||||||
echo ""
|
echo ""
|
||||||
echo "📌 Odoo — Assets + DB + Config..."
|
echo "📌 Odoo — Assets + DB + Config..."
|
||||||
if container_is_healthy odoo-clean-odoo-1; then
|
if ! app_selected odoo; then
|
||||||
|
echo " ⏭️ Odoo not selected, skipping"
|
||||||
|
elif [ "$APPS_FILTER" = "all" ] && container_is_healthy odoo-clean-odoo-1; then
|
||||||
echo " ⏭️ Odoo is healthy, skipping fix"
|
echo " ⏭️ Odoo is healthy, skipping fix"
|
||||||
else
|
else
|
||||||
if docker ps | grep -q odoo-clean-db-1; then
|
if docker ps | grep -q odoo-clean-db-1; then
|
||||||
@@ -325,7 +418,9 @@ fi
|
|||||||
# ---- FRAPPE ----
|
# ---- FRAPPE ----
|
||||||
echo ""
|
echo ""
|
||||||
echo "📌 Frappe — DB permissions + cache + URL..."
|
echo "📌 Frappe — DB permissions + cache + URL..."
|
||||||
if container_is_healthy frappe-erpnext; then
|
if ! app_selected frappe; then
|
||||||
|
echo " ⏭️ Frappe not selected, skipping"
|
||||||
|
elif [ "$APPS_FILTER" = "all" ] && container_is_healthy frappe-erpnext; then
|
||||||
echo " ⏭️ Frappe is healthy, skipping fix"
|
echo " ⏭️ Frappe is healthy, skipping fix"
|
||||||
else
|
else
|
||||||
if docker ps | grep -q frappe-erpnext && docker ps | grep -q frappe-mariadb; then
|
if docker ps | grep -q frappe-erpnext && docker ps | grep -q frappe-mariadb; then
|
||||||
@@ -363,7 +458,9 @@ fi
|
|||||||
# ---- N8N ----
|
# ---- N8N ----
|
||||||
echo ""
|
echo ""
|
||||||
echo "📌 n8n — Network check..."
|
echo "📌 n8n — Network check..."
|
||||||
if container_is_healthy n8n-app; then
|
if ! app_selected n8n; then
|
||||||
|
echo " ⏭️ n8n not selected, skipping"
|
||||||
|
elif [ "$APPS_FILTER" = "all" ] && container_is_healthy n8n-app; then
|
||||||
echo " ⏭️ n8n is healthy, skipping fix"
|
echo " ⏭️ n8n is healthy, skipping fix"
|
||||||
else
|
else
|
||||||
docker network inspect integration-network &>/dev/null \
|
docker network inspect integration-network &>/dev/null \
|
||||||
@@ -388,9 +485,9 @@ echo ""
|
|||||||
echo "========================================="
|
echo "========================================="
|
||||||
echo "✅ RESTORE COMPLETE"
|
echo "✅ RESTORE COMPLETE"
|
||||||
echo "========================================="
|
echo "========================================="
|
||||||
echo " Nextcloud → http://${VM_IP}:8082"
|
if app_selected nextcloud; then echo " Nextcloud → http://${VM_IP}:8082"; fi
|
||||||
echo " Mautic → http://${VM_IP}:8081/s/login (admin / Admin!Password123)"
|
if app_selected mautic; then echo " Mautic → http://${VM_IP}:8081/s/login (admin / Admin!Password123)"; fi
|
||||||
echo " Odoo → https://odooo.nav.ovh/web"
|
if app_selected odoo; then echo " Odoo → https://odooo.nav.ovh/web"; fi
|
||||||
echo " n8n → http://${VM_IP}:5678"
|
if app_selected n8n; then echo " n8n → http://${VM_IP}:5678"; fi
|
||||||
echo " Frappe → http://${VM_IP}:8080"
|
if app_selected frappe; then echo " Frappe → http://${VM_IP}:8080"; fi
|
||||||
echo "========================================="
|
echo "========================================="
|
||||||
@@ -113,11 +113,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="form-section-title">STEP 3 — SELECT APPS TO RESTORE</div>
|
||||||
|
<p style="color:var(--text3); font-size:12px; margin-bottom:10px;">
|
||||||
|
All apps are restored by default. Uncheck the ones you don't want touched —
|
||||||
|
only the checked apps will be restored, and they're force-restored even if
|
||||||
|
currently running and healthy.
|
||||||
|
</p>
|
||||||
|
<div id="apps-checkbox-group" style="display:flex; flex-wrap:wrap; gap:8px;">
|
||||||
|
<label class="radio-card small" style="cursor:pointer;">
|
||||||
|
<input type="checkbox" value="frappe" checked style="margin-right:6px;">
|
||||||
|
<span>🧾 Frappe / ERPNext</span>
|
||||||
|
</label>
|
||||||
|
<label class="radio-card small" style="cursor:pointer;">
|
||||||
|
<input type="checkbox" value="odoo" checked style="margin-right:6px;">
|
||||||
|
<span>🟣 Odoo</span>
|
||||||
|
</label>
|
||||||
|
<label class="radio-card small" style="cursor:pointer;">
|
||||||
|
<input type="checkbox" value="nextcloud" checked style="margin-right:6px;">
|
||||||
|
<span>☁️ Nextcloud</span>
|
||||||
|
</label>
|
||||||
|
<label class="radio-card small" style="cursor:pointer;">
|
||||||
|
<input type="checkbox" value="mautic" checked style="margin-right:6px;">
|
||||||
|
<span>📣 Mautic</span>
|
||||||
|
</label>
|
||||||
|
<label class="radio-card small" style="cursor:pointer;">
|
||||||
|
<input type="checkbox" value="n8n" checked style="margin-right:6px;">
|
||||||
|
<span>🔁 n8n</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:10px; display:flex; gap:8px;">
|
||||||
|
<button type="button" class="btn btn-sm" onclick="nvSetAllApps(true)">Select All</button>
|
||||||
|
<button type="button" class="btn btn-sm" onclick="nvSetAllApps(false)">Select None</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<button class="btn btn-danger btn-lg" onclick="nvLaunchRestore()" id="restore-btn">
|
<button class="btn btn-danger btn-lg" onclick="nvLaunchRestore()" id="restore-btn">
|
||||||
<i class="fas fa-play"></i> Start Restore
|
<i class="fas fa-play"></i> Start Restore
|
||||||
</button>
|
</button>
|
||||||
<p style="color:var(--text3); font-size:12px; margin-top:8px;">⚠ Healthy running containers are skipped.</p>
|
<p style="color:var(--text3); font-size:12px; margin-top:8px;" id="restore-warning-text">
|
||||||
|
⚠ Full restore (all apps checked): healthy running containers are skipped.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -182,11 +219,48 @@ function nvGetSelection() {
|
|||||||
return { source, file, key };
|
return { source, file, key };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── App selection (Step 3) ────────────────────────────────────────────────────
|
||||||
|
function nvGetAppsCheckboxes() {
|
||||||
|
return Array.from(document.querySelectorAll('#apps-checkbox-group input[type="checkbox"]'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns [] when every app is checked (= full restore, no filter needed),
|
||||||
|
// otherwise returns the list of explicitly checked app keys.
|
||||||
|
function nvGetSelectedApps() {
|
||||||
|
const boxes = nvGetAppsCheckboxes();
|
||||||
|
const checked = boxes.filter(b => b.checked).map(b => b.value);
|
||||||
|
return checked.length === boxes.length ? [] : checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nvSetAllApps(state) {
|
||||||
|
nvGetAppsCheckboxes().forEach(b => b.checked = state);
|
||||||
|
nvUpdateRestoreWarning();
|
||||||
|
}
|
||||||
|
|
||||||
|
function nvUpdateRestoreWarning() {
|
||||||
|
const selected = nvGetSelectedApps();
|
||||||
|
const el = document.getElementById('restore-warning-text');
|
||||||
|
if (selected.length === 0) {
|
||||||
|
el.textContent = '⚠ Full restore (all apps checked): healthy running containers are skipped.';
|
||||||
|
} else {
|
||||||
|
el.textContent = `⚠ Selective restore: only ${selected.join(', ')} will be touched — forced even if currently healthy.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('change', (e) => {
|
||||||
|
if (e.target.closest('#apps-checkbox-group')) nvUpdateRestoreWarning();
|
||||||
|
});
|
||||||
|
|
||||||
// ── Launch restore ────────────────────────────────────────────────────────────
|
// ── Launch restore ────────────────────────────────────────────────────────────
|
||||||
async function nvLaunchRestore() {
|
async function nvLaunchRestore() {
|
||||||
const { source, file, key } = nvGetSelection();
|
const { source, file, key } = nvGetSelection();
|
||||||
if (!file) { alert('Please select a backup file.'); return; }
|
if (!file) { alert('Please select a backup file.'); return; }
|
||||||
|
|
||||||
|
const appsCheckboxes = nvGetAppsCheckboxes();
|
||||||
|
const checkedCount = appsCheckboxes.filter(b => b.checked).length;
|
||||||
|
if (checkedCount === 0) { alert('Select at least one app to restore.'); return; }
|
||||||
|
const selectedApps = nvGetSelectedApps();
|
||||||
|
|
||||||
const target = document.querySelector('input[name="restore_target"]:checked')?.value || 'local';
|
const target = document.querySelector('input[name="restore_target"]:checked')?.value || 'local';
|
||||||
const authMethod = document.querySelector('input[name="auth_method"]:checked')?.value || 'key';
|
const authMethod = document.querySelector('input[name="auth_method"]:checked')?.value || 'key';
|
||||||
|
|
||||||
@@ -194,6 +268,7 @@ async function nvLaunchRestore() {
|
|||||||
backup_source: source,
|
backup_source: source,
|
||||||
backup_file: file,
|
backup_file: file,
|
||||||
cloud_key: key,
|
cloud_key: key,
|
||||||
|
apps: selectedApps,
|
||||||
target,
|
target,
|
||||||
remote_ip: document.getElementById('remote-ip')?.value || '',
|
remote_ip: document.getElementById('remote-ip')?.value || '',
|
||||||
remote_port: document.getElementById('remote-port')?.value || '22',
|
remote_port: document.getElementById('remote-port')?.value || '22',
|
||||||
@@ -211,9 +286,12 @@ async function nvLaunchRestore() {
|
|||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
wrapper.style.display = '';
|
wrapper.style.display = '';
|
||||||
|
const scopeMsg = selectedApps.length
|
||||||
|
? `🎯 Restoring only: ${selectedApps.join(', ')}`
|
||||||
|
: '🚀 Starting full restore…';
|
||||||
logEl.innerHTML = source === 'cloud'
|
logEl.innerHTML = source === 'cloud'
|
||||||
? '<div style="color:var(--accent2)">☁️ Downloading backup from R2, please wait…</div>'
|
? `<div style="color:var(--accent2)">☁️ Downloading backup from R2, please wait…</div><div style="color:var(--accent2)">${scopeMsg}</div>`
|
||||||
: '<div style="color:var(--accent2)">🚀 Starting restore…</div>';
|
: `<div style="color:var(--accent2)">${scopeMsg}</div>`;
|
||||||
badge.textContent = 'Running…';
|
badge.textContent = 'Running…';
|
||||||
badge.style.cssText = 'background:rgba(59,130,246,0.15);color:var(--accent2);';
|
badge.style.cssText = 'background:rgba(59,130,246,0.15);color:var(--accent2);';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user