From cc72968583147fb4638a772bd8b944206df445d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 03:41:53 +0000 Subject: [PATCH] Add Manual Backup improvements: lsblk drive filtering, UI instructions, CSS border fixes Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/a43d270d-eb78-4ad3-b721-fe958883c305 Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com> --- .../scripts/sovran-hub-backup.sh | 122 +++++++++++++++--- app/sovran_systemsos_web/server.py | 103 ++++++++++++++- .../static/css/layout.css | 8 +- .../static/css/support.css | 8 +- app/sovran_systemsos_web/static/js/support.js | 23 +++- 5 files changed, 232 insertions(+), 32 deletions(-) diff --git a/app/sovran_systemsos_web/scripts/sovran-hub-backup.sh b/app/sovran_systemsos_web/scripts/sovran-hub-backup.sh index 2627f2b..e4500ce 100755 --- a/app/sovran_systemsos_web/scripts/sovran-hub-backup.sh +++ b/app/sovran_systemsos_web/scripts/sovran-hub-backup.sh @@ -1,11 +1,16 @@ #!/usr/bin/env bash -# ── Sovran Hub Backup Script ───────────────────────────────────── +# ── Sovran Hub External Backup Script ──────────────────────────── # Backs up Sovran_SystemsOS data to an external USB hard drive. # Designed for the Hub web UI (no GUI dependencies). # +# Your Sovran Pro already backs up your data automatically to its +# internal second drive (BTCEcoandBackup at /run/media/Second_Drive). +# This script creates an additional copy on an external USB drive — +# storing your data in a third location for maximum protection. +# # Usage: # BACKUP_TARGET=/run/media// bash sovran-hub-backup.sh -# (or run with no env var to auto-detect the first external drive) +# (or run with no env var to auto-detect the first external USB drive) set -euo pipefail @@ -14,6 +19,10 @@ BACKUP_STATUS="/var/log/sovran-hub-backup.status" MEDIA_ROOT="/run/media" MIN_FREE_GB=10 +# ── Internal drive labels/paths to NEVER use as backup targets ─── +INTERNAL_LABELS=("BTCEcoandBackup" "sovran_systemsos") +INTERNAL_MOUNTS=("/run/media/Second_Drive" "/boot/efi" "/") + # ── Logging helpers ────────────────────────────────────────────── log() { @@ -31,33 +40,113 @@ fail() { exit 1 } +# ── Check whether a mount point is an internal drive ──────────── + +is_internal() { + local mnt="$1" + # Reject known internal mount points and their subdirectories + for internal in "${INTERNAL_MOUNTS[@]}"; do + if [[ "$mnt" == "$internal" || "$mnt" == "${internal}/"* ]]; then + return 0 + fi + done + return 1 +} + +# ── Use lsblk to find the first genuine external USB drive ─────── + +find_external_drive() { + local target="" + # lsblk JSON output: NAME,LABEL,MOUNTPOINT,HOTPLUG,RM,TYPE + if command -v lsblk &>/dev/null; then + while IFS=$'\t' read -r dev_type hotplug removable label mountpoint; do + # Must be a partition or disk, and be removable/hotplug + [[ "$dev_type" == "part" || "$dev_type" == "disk" ]] || continue + [[ "$hotplug" == "1" || "$removable" == "1" ]] || continue + [[ -n "$mountpoint" ]] || continue + + # Filter out internal labels + local skip=0 + for lbl in "${INTERNAL_LABELS[@]}"; do + [[ "$label" == "$lbl" ]] && skip=1 && break + done + [[ "$skip" -eq 1 ]] && continue + + # Filter out internal mount points + is_internal "$mountpoint" && continue + + if mountpoint -q "$mountpoint" 2>/dev/null; then + target="$mountpoint" + break + fi + done < <(lsblk -J -o NAME,LABEL,MOUNTPOINT,HOTPLUG,RM,TYPE 2>/dev/null | \ + python3 -c " +import sys, json +data = json.load(sys.stdin) +def flatten(devs): + for d in devs: + yield d + for c in d.get('children', []): + yield from flatten([c]) +for d in flatten(data.get('blockdevices', [])): + print('\t'.join([ + d.get('type') or '', + str(d.get('hotplug') or '0'), + str(d.get('rm') or '0'), + d.get('label') or '', + d.get('mountpoint') or '', + ])) +" 2>/dev/null || true) + fi + + # Fallback: walk /run/media/ if lsblk produced nothing + if [[ -z "$target" && -d "$MEDIA_ROOT" ]]; then + while IFS= read -r -d '' mnt; do + is_internal "$mnt" && continue + # Check label via lsblk on the device backing this mount + local dev + dev=$(findmnt -n -o SOURCE "$mnt" 2>/dev/null || true) + if [[ -n "$dev" ]]; then + local lbl + lbl=$(lsblk -n -o LABEL "$dev" 2>/dev/null || true) + local skip=0 + for internal_lbl in "${INTERNAL_LABELS[@]}"; do + [[ "$lbl" == "$internal_lbl" ]] && skip=1 && break + done + [[ "$skip" -eq 1 ]] && continue + fi + if mountpoint -q "$mnt" 2>/dev/null; then + target="$mnt" + break + fi + done < <(find "$MEDIA_ROOT" -mindepth 2 -maxdepth 2 -type d -print0 2>/dev/null) + fi + + echo "$target" +} + # ── Initialise log file ────────────────────────────────────────── : > "$BACKUP_LOG" set_status "RUNNING" -log "=== Sovran_SystemsOS Hub Backup ===" +log "=== Sovran_SystemsOS External Hub Backup ===" log "Starting backup process…" # ── Detect target drive ────────────────────────────────────────── if [[ -n "${BACKUP_TARGET:-}" ]]; then TARGET="$BACKUP_TARGET" + # Safety: never allow internal drives even if explicitly passed + if is_internal "$TARGET"; then + fail "Target '$TARGET' is an internal system drive and cannot be used for external backup." + fi log "Using specified backup target: $TARGET" else - log "Auto-detecting external drives under $MEDIA_ROOT …" - TARGET="" - if [[ -d "$MEDIA_ROOT" ]]; then - # Walk /run/media// - while IFS= read -r -d '' mnt; do - if mountpoint -q "$mnt" 2>/dev/null; then - TARGET="$mnt" - break - fi - done < <(find "$MEDIA_ROOT" -mindepth 2 -maxdepth 2 -type d -print0 2>/dev/null) - fi + log "Auto-detecting external USB drives…" + TARGET="$(find_external_drive)" if [[ -z "$TARGET" ]]; then - fail "No external drive detected under $MEDIA_ROOT. " \ + fail "No external USB drive detected. " \ "Please plug in an exFAT-formatted USB drive (≥500 GB) and try again." fi log "Detected external drive: $TARGET" @@ -169,5 +258,6 @@ log "Manifest written to $BACKUP_DIR/BACKUP_MANIFEST.txt" # ── Done ───────────────────────────────────────────────────────── log "" -log "All Finished! Please eject the drive before removing it from your Sovran Pro." +log "All Finished! Your data is now backed up to a third location." +log "Please eject the drive safely before removing it from your Sovran Pro." set_status "SUCCESS" diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index 4babf70..8348b17 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -774,10 +774,94 @@ def _read_backup_log(offset: int = 0) -> tuple[str, int]: return "", 0 +_INTERNAL_LABELS = {"BTCEcoandBackup", "sovran_systemsos"} +_INTERNAL_MOUNTS = {"/", "/boot/efi"} +_INTERNAL_MOUNT_PREFIX = "/run/media/Second_Drive" + + +def _is_internal_mount(mnt: str) -> bool: + """Return True if *mnt* is a known internal system path.""" + if mnt in _INTERNAL_MOUNTS: + return True + if mnt == _INTERNAL_MOUNT_PREFIX or mnt.startswith(_INTERNAL_MOUNT_PREFIX + "/"): + return True + return False + + def _detect_external_drives() -> list[dict]: - """Scan /run/media/ for mounted external drives. - Returns a list of dicts with name, path, free_gb, total_gb.""" - drives = [] + """Scan for mounted external USB drives. + + Uses ``lsblk`` to identify genuinely removable/hotplug devices and + filters out internal system drives (BTCEcoandBackup, sovran_systemsos, + /boot/efi, /run/media/Second_Drive). Falls back to scanning + /run/media/ directly if lsblk is unavailable, applying the same + label/path filters. + + Returns a list of dicts with name, path, free_gb, total_gb. + """ + import json as _json + import subprocess as _subprocess + + drives: list[dict] = [] + seen_paths: set[str] = set() + + # ── Primary path: lsblk JSON ──────────────────────────────── + try: + result = _subprocess.run( + ["lsblk", "-J", "-o", "NAME,LABEL,MOUNTPOINT,HOTPLUG,RM,TYPE"], + capture_output=True, text=True, timeout=10 + ) + if result.returncode == 0: + data = _json.loads(result.stdout) + + def _flatten(devs: list) -> list: + out = [] + for d in devs: + out.append(d) + out.extend(_flatten(d.get("children") or [])) + return out + + for dev in _flatten(data.get("blockdevices", [])): + dev_type = dev.get("type", "") + hotplug = str(dev.get("hotplug", "0")) + rm = str(dev.get("rm", "0")) + label = dev.get("label") or "" + mountpoint = dev.get("mountpoint") or "" + + if dev_type not in ("part", "disk"): + continue + if hotplug != "1" and rm != "1": + continue + if not mountpoint: + continue + if label in _INTERNAL_LABELS: + continue + if _is_internal_mount(mountpoint): + continue + if mountpoint in seen_paths: + continue + + try: + st = os.statvfs(mountpoint) + total_gb = round((st.f_blocks * st.f_frsize) / (1024 ** 3), 1) + free_gb = round((st.f_bavail * st.f_frsize) / (1024 ** 3), 1) + name = label if label else os.path.basename(mountpoint) + drives.append({ + "name": name, + "path": mountpoint, + "free_gb": free_gb, + "total_gb": total_gb, + }) + seen_paths.add(mountpoint) + except OSError: + pass + + if drives: + return drives + except Exception: # lsblk not available or JSON parse error + pass + + # ── Fallback: scan /run/media/ ─────────────────────────────── media_root = "/run/media" if not os.path.isdir(media_root): return drives @@ -786,20 +870,27 @@ def _detect_external_drives() -> list[dict]: user_path = os.path.join(media_root, user) if not os.path.isdir(user_path): continue - for drive in os.listdir(user_path): - drive_path = os.path.join(user_path, drive) + for drive_name in os.listdir(user_path): + drive_path = os.path.join(user_path, drive_name) if not os.path.isdir(drive_path): continue + if drive_name in _INTERNAL_LABELS: + continue + if _is_internal_mount(drive_path): + continue + if drive_path in seen_paths: + continue try: st = os.statvfs(drive_path) total_gb = round((st.f_blocks * st.f_frsize) / (1024 ** 3), 1) free_gb = round((st.f_bavail * st.f_frsize) / (1024 ** 3), 1) drives.append({ - "name": drive, + "name": drive_name, "path": drive_path, "free_gb": free_gb, "total_gb": total_gb, }) + seen_paths.add(drive_path) except OSError: pass except OSError: diff --git a/app/sovran_systemsos_web/static/css/layout.css b/app/sovran_systemsos_web/static/css/layout.css index 1edb4e5..090d544 100644 --- a/app/sovran_systemsos_web/static/css/layout.css +++ b/app/sovran_systemsos_web/static/css/layout.css @@ -34,7 +34,7 @@ gap: 10px; width: 100%; background-color: var(--card-color); - border: 2px dashed var(--accent-color); + border: 1px solid var(--border-color); border-radius: 12px; padding: 12px 14px; color: var(--text-primary); @@ -44,11 +44,15 @@ } .sidebar-support-btn:hover { + border-color: var(--accent-color); border-style: solid; - border-color: #a8c8ff; background-color: #35354a; } +.sidebar-support-btn + .sidebar-support-btn { + margin-top: 8px; +} + .sidebar-support-icon { font-size: 1.5rem; flex-shrink: 0; diff --git a/app/sovran_systemsos_web/static/css/support.css b/app/sovran_systemsos_web/static/css/support.css index e081579..3006a59 100644 --- a/app/sovran_systemsos_web/static/css/support.css +++ b/app/sovran_systemsos_web/static/css/support.css @@ -351,13 +351,13 @@ /* ── Tech Support tile ───────────────────────────────────────────── */ .support-tile { - border-color: var(--accent-color); - border-width: 2px; - border-style: dashed; + border-color: var(--border-color); + border-width: 1px; + border-style: solid; } .support-tile:hover { - border-color: #a8c8ff; + border-color: var(--accent-color); border-style: solid; } diff --git a/app/sovran_systemsos_web/static/js/support.js b/app/sovran_systemsos_web/static/js/support.js index c0e816e..0735b12 100644 --- a/app/sovran_systemsos_web/static/js/support.js +++ b/app/sovran_systemsos_web/static/js/support.js @@ -301,7 +301,8 @@ function renderBackupReady(drives) { if (drives.length > 0) { driveSelector = [ '', - '', ].join(""); for (var i = 0; i < drives.length; i++) { var d = drives[i]; @@ -310,6 +311,8 @@ function renderBackupReady(drives) { ''; } driveSelector += ''; + driveSelector += ''; + driveSelector += ''; driveSelector += ''; } else { driveSelector = [ @@ -331,7 +334,15 @@ function renderBackupReady(drives) { '
', '
\ud83d\udcbe
', '

Manual Backup

', - '

Back up your Sovran_SystemsOS data to an external USB hard drive.

', + + '
', + '

', + 'Your Sovran Pro already backs up your data automatically to its internal second drive. ', + 'This manual backup lets you create an additional copy on an external USB drive \u2014 ', + 'storing your data in a third location, outside the computer, for maximum protection ', + 'against hardware failure or physical damage.', + '

', + '
', '
', '
Requirements
', @@ -353,7 +364,7 @@ function renderBackupReady(drives) { '', '
', - '
', + '
', '
', '\u23f1\ufe0f', 'Time Estimate', @@ -367,9 +378,13 @@ function renderBackupReady(drives) { if (drives.length > 0) { document.getElementById("btn-start-backup").addEventListener("click", startBackup); + document.getElementById("btn-backup-refresh").addEventListener("click", function() { + $supportBody.innerHTML = '

Scanning for external drives\u2026

'; + detectDrivesAndRender(); + }); } else { document.getElementById("btn-backup-refresh").addEventListener("click", function() { - $supportBody.innerHTML = '

Detecting external drives\u2026

'; + $supportBody.innerHTML = '

Scanning for external drives\u2026

'; detectDrivesAndRender(); }); }