From 81347bbdd29fca0f6480dd5faeb6fe56988f9afd Mon Sep 17 00:00:00 2001 From: root Date: Thu, 16 Apr 2026 13:42:46 +0200 Subject: [PATCH] Sync from main server - 2026-04-16 13:42:46 --- platform/app.py | 136 +++++-- platform/modules/backups.py | 248 +++++++++---- platform/templates/dashboard.html | 583 ++++++++++++++---------------- 3 files changed, 547 insertions(+), 420 deletions(-) diff --git a/platform/app.py b/platform/app.py index d9e50b0..fd59c90 100644 --- a/platform/app.py +++ b/platform/app.py @@ -1,19 +1,28 @@ +# app.py from flask import Flask, render_template, request, redirect, url_for, session, jsonify import os import subprocess import threading import uuid import time -from config import MAIN_SERVER_IP, VM_HOST, VM_PORT, VM_KEY, VM_USER + +from config import ( + MAIN_SERVER_IP, RUNNING_ON_MAIN_SERVER, + VM_HOST, VM_PORT, VM_KEY, VM_USER, + MAIN_SERVER_KEY, MAIN_SERVER_PORT, MAIN_SERVER_USER, +) from modules.auth import login_required from modules.backups import ( - get_containers, get_all_root_containers, get_local_backups, - get_vm_backups, get_all_stats, get_system_info + get_containers, get_all_root_containers, + get_local_backups, get_vm_backups, + get_all_stats, get_system_info, + get_rootless_user_containers_remote, + container_action, ) -from modules.commands import run_command, run_ssh_to_vm +from modules.commands import run_command from modules.users import ( get_all_users, get_user_containers, get_all_users_containers, - create_user, delete_user, get_user_disk_usage + create_user, delete_user, get_user_disk_usage, ) app = Flask(__name__) @@ -46,12 +55,13 @@ def _stream_restore(job_id, cmd): @app.route('/') @login_required def dashboard(): - containers = get_containers() + containers = get_containers() running_count = sum(1 for c in containers if 'Up' in c.get('status', '')) - backups = get_local_backups() - vm_backups = get_vm_backups() - system = get_system_info() - users = get_all_users() + backups = get_local_backups() + vm_backups = get_vm_backups() + system = get_system_info() + # Users are still LOCAL (users on the platform host) + users = get_all_users() return render_template('dashboard.html', containers=containers, running_count=running_count, @@ -59,11 +69,12 @@ def dashboard(): vm_backups=vm_backups, main_server=MAIN_SERVER_IP, system=system, - users=users) + users=users, + running_on_main=RUNNING_ON_MAIN_SERVER) # ───────────────────────────────────────────── -# API — system info + stats (live poll) +# API — system + stats (always from main server) # ───────────────────────────────────────────── @app.route('/api/system') @login_required @@ -74,14 +85,13 @@ def api_system(): @app.route('/api/stats') @login_required def api_stats(): - """Container resource stats for ALL containers (root + rootless users).""" return jsonify(get_all_stats()) @app.route('/api/containers') @login_required def api_containers(): - containers = get_all_root_containers() + containers = get_all_root_containers() running_count = sum(1 for c in containers if 'Up' in c.get('status', '')) return jsonify({'containers': containers, 'running': running_count}) @@ -89,14 +99,35 @@ def api_containers(): @app.route('/api/containers/all') @login_required def api_containers_all(): - """Root containers + all users' rootless containers combined.""" + """Root containers + rootless-user containers, all from main server.""" root_ctrs = get_all_root_containers() - user_ctrs = get_all_users_containers() + user_ctrs = get_rootless_user_containers_remote() all_ctrs = root_ctrs + user_ctrs running = sum(1 for c in all_ctrs if 'Up' in c.get('status', '')) return jsonify({'containers': all_ctrs, 'running': running}) +# ───────────────────────────────────────────── +# API — container actions +# ───────────────────────────────────────────── +@app.route('/api/container/action', methods=['POST']) +@login_required +def api_container_action(): + """ + POST JSON: { "name": "container-name", "action": "start|stop|restart" } + Runs the action on the main server (via SSH if on VM). + """ + data = request.get_json() or {} + name = data.get('name', '').strip() + action = data.get('action', '').strip() + + if not name or not action: + return jsonify({'success': False, 'message': 'name and action required'}), 400 + + success, output = container_action(name, action) + return jsonify({'success': success, 'output': output}) + + # ───────────────────────────────────────────── # API — backups # ───────────────────────────────────────────── @@ -107,7 +138,7 @@ def api_backups(): # ───────────────────────────────────────────── -# API — users management +# API — users (LOCAL — users on this host) # ───────────────────────────────────────────── @app.route('/api/users') @login_required @@ -130,7 +161,7 @@ def api_user_disk(username): @app.route('/api/users/create', methods=['POST']) @login_required def api_create_user(): - data = request.get_json() or {} + data = request.get_json() or {} username = data.get('username', '').strip() password = data.get('password', '').strip() setup_docker = data.get('setup_docker', True) @@ -151,7 +182,7 @@ def api_create_user(): @app.route('/api/users/delete', methods=['POST']) @login_required def api_delete_user(): - data = request.get_json() or {} + data = request.get_json() or {} username = data.get('username', '').strip() remove_home = data.get('remove_home', False) @@ -185,22 +216,46 @@ def restore_start(): if not backup_file: return jsonify({'error': 'No backup file specified'}), 400 - # Resolve archive path on this server + # ── Resolve backup archive path ────────────────────────────────────────── if backup_source == 'local': - backup_path = f"/root/backups/{backup_file}" - if not os.path.exists(backup_path): - return jsonify({'error': f'Backup not found: {backup_path}'}), 400 + # Backup is on main server at /root/backups/ + if RUNNING_ON_MAIN_SERVER: + backup_path = f"/root/backups/{backup_file}" + if not os.path.exists(backup_path): + return jsonify({'error': f'Not found: {backup_path}'}), 400 + else: + # We're on VM → need to pull backup from main server to /tmp/ first + backup_path = f"/tmp/{backup_file}" + if not os.path.exists(backup_path): + pull_cmd = ( + f"scp -i {MAIN_SERVER_KEY} -P {MAIN_SERVER_PORT} " + f"-o StrictHostKeyChecking=no -o ConnectTimeout=15 " + f"{MAIN_SERVER_USER}@{MAIN_SERVER_IP}:/root/backups/{backup_file} " + f"{backup_path}" + ) + res = subprocess.run(pull_cmd, shell=True, capture_output=True, text=True) + if res.returncode != 0: + return jsonify({'error': f'Failed to pull from main server: {res.stderr}'}), 500 else: - backup_path = f"/tmp/{backup_file}" - if not os.path.exists(backup_path): - pull_cmd = ( - f"scp -i {VM_KEY} -P {VM_PORT} " - f"-o StrictHostKeyChecking=no -o ConnectTimeout=15 " - f"{VM_USER}@{VM_HOST}:/backups/main-server/{backup_file} {backup_path}" - ) - result = subprocess.run(pull_cmd, shell=True, capture_output=True, text=True) - if result.returncode != 0: - return jsonify({'error': f'Failed to pull from VM: {result.stderr}'}), 500 + # VM backup + if RUNNING_ON_MAIN_SERVER: + # Pull from VM via tunnel + backup_path = f"/tmp/{backup_file}" + if not os.path.exists(backup_path): + pull_cmd = ( + f"scp -i {VM_KEY} -P {VM_PORT} " + f"-o StrictHostKeyChecking=no -o ConnectTimeout=15 " + f"{VM_USER}@{VM_HOST}:/backups/main-server/{backup_file} " + f"{backup_path}" + ) + res = subprocess.run(pull_cmd, shell=True, capture_output=True, text=True) + if res.returncode != 0: + return jsonify({'error': f'Failed to pull from VM: {res.stderr}'}), 500 + else: + # We're on VM → backup is local + backup_path = f"/backups/main-server/{backup_file}" + if not os.path.exists(backup_path): + return jsonify({'error': f'Not found: {backup_path}'}), 400 restore_script_local = os.path.join( os.path.dirname(os.path.abspath(__file__)), 'restore-myapps.sh' @@ -208,20 +263,28 @@ def restore_start(): if not os.path.exists(restore_script_local): return jsonify({'error': f'restore-myapps.sh not found at {restore_script_local}'}), 500 + # ── Determine the actual target ────────────────────────────────────────── + # "local" always means THIS host — wherever the platform is currently deployed. + # Run the restore script directly, no SSH indirection needed. if target == 'local': + hostname = os.uname().nodename session_dir = f"/tmp/restore-session-{uuid.uuid4().hex[:8]}" cmd = ( - f"set -e && mkdir -p {session_dir} && " - f"echo '📂 Extracting backup locally...' && " + f"set -e && " + f"echo '🖥️ Restoring on this server ({hostname})...' && " + f"mkdir -p {session_dir} && " + f"echo '📂 Extracting backup...' && " f"tar -xzf {backup_path} -C {session_dir} --strip-components=1 && " f"cp {restore_script_local} {session_dir}/restore-myapps.sh && " f"chmod +x {session_dir}/restore-myapps.sh && " f"cd {session_dir} && bash restore-myapps.sh ; " f"EXIT=$? ; rm -rf {session_dir} ; exit $EXIT" ) + else: + # Explicit remote machine (custom IP) if not remote_ip: - return jsonify({'error': 'remote_ip required for remote restore'}), 400 + return jsonify({'error': 'remote_ip required'}), 400 base_opts = "-o StrictHostKeyChecking=no -o ConnectTimeout=15" if auth_method == 'key': @@ -247,7 +310,6 @@ def restore_start(): f"echo '🚀 Running restore on {remote_ip}:{remote_port}...' && " f"{ssh_prefix} {remote_user}@{remote_ip} " f"'set -e && cd {remote_dest} && " - f"echo \"📂 Extracting backup...\" && " f"tar -xzf {backup_file} --strip-components=1 && " f"chmod +x restore-myapps.sh && bash restore-myapps.sh' ; " f"EXIT=$? ; " diff --git a/platform/modules/backups.py b/platform/modules/backups.py index 1eef9b5..8424146 100644 --- a/platform/modules/backups.py +++ b/platform/modules/backups.py @@ -1,8 +1,13 @@ +# modules/backups.py import os import glob import subprocess import json -from config import RUNNING_ON_MAIN_SERVER, VM_HOST, VM_PORT, VM_KEY, VM_USER +from config import ( + RUNNING_ON_MAIN_SERVER, + MAIN_SERVER_IP, MAIN_SERVER_USER, MAIN_SERVER_KEY, MAIN_SERVER_PORT, + VM_HOST, VM_PORT, VM_KEY, VM_USER, +) def _run(cmd, timeout=20): @@ -13,12 +18,35 @@ def _run(cmd, timeout=20): return '', str(e) +def _ssh_main(remote_cmd, timeout=20): + """ + Run a command ON THE MAIN SERVER. + - If we're already on the main server: run it locally. + - If we're on the VM: SSH to main server first. + """ + if RUNNING_ON_MAIN_SERVER: + return _run(remote_cmd, timeout=timeout) + else: + ssh = ( + f"ssh -i {MAIN_SERVER_KEY} -p {MAIN_SERVER_PORT} " + f"-o StrictHostKeyChecking=no -o ConnectTimeout=10 " + f"{MAIN_SERVER_USER}@{MAIN_SERVER_IP}" + ) + return _run(f"{ssh} '{remote_cmd}'", timeout=timeout) + + # ──────────────────────────────────────────────────────────────── -# BACKUPS +# BACKUPS (local = on main server; vm = on the VM) # ──────────────────────────────────────────────────────────────── def get_local_backups(): - stdout, _ = _run("ls -t /root/backups/myapps-backup-*.tar.gz 2>/dev/null | head -20") + """ + Backups stored on the MAIN SERVER at /root/backups/. + Always fetched from main server regardless of where platform runs. + """ + stdout, _ = _ssh_main( + "ls -t /root/backups/myapps-backup-*.tar.gz 2>/dev/null | head -20" + ) files = [] if stdout: for line in stdout.split('\n'): @@ -29,7 +57,13 @@ def get_local_backups(): def get_vm_backups(): + """ + Backups stored on the VM at /backups/main-server/. + - On main server → SSH through tunnel (localhost:2223) + - On VM → read local directory directly + """ vm_backups = [] + if RUNNING_ON_MAIN_SERVER: try: cmd = ( @@ -38,7 +72,7 @@ def get_vm_backups(): f"{VM_USER}@{VM_HOST} " f"'ls -t /backups/main-server/myapps-backup-*.tar.gz 2>/dev/null | head -20'" ) - stdout, _ = _run(cmd, timeout=20) + stdout, _ = _run(cmd, timeout=25) if stdout: for line in stdout.split('\n'): line = line.strip() @@ -47,80 +81,121 @@ def get_vm_backups(): except Exception as e: print(f"[backups] VM backup fetch error: {e}") else: + # We ARE on the VM — read directly backup_dir = '/backups/main-server' if os.path.exists(backup_dir): files = glob.glob(f'{backup_dir}/myapps-backup-*.tar.gz') files.sort(key=os.path.getmtime, reverse=True) vm_backups = [os.path.basename(f) for f in files[:20]] + return vm_backups # ──────────────────────────────────────────────────────────────── -# ROOT CONTAINERS +# CONTAINERS (always from main server) # ──────────────────────────────────────────────────────────────── +def _parse_containers(raw, owner='root'): + containers = [] + if raw: + for line in raw.split('\n'): + if '|' not in line: + continue + parts = line.split('|') + containers.append({ + 'name': parts[0].strip(), + 'status': parts[1].strip() if len(parts) > 1 else '', + 'image': parts[2].strip() if len(parts) > 2 else '', + 'ports': parts[3].strip() if len(parts) > 3 else '', + 'owner': owner, + }) + return containers + + def get_containers(): - """Root app containers only (filtered).""" - stdout, _ = _run( + """App containers only (frappe/nextcloud/mautic/n8n/odoo) — always from main server.""" + stdout, _ = _ssh_main( "docker ps -a --format '{{.Names}}|{{.Status}}|{{.Image}}|{{.Ports}}' 2>/dev/null | " "grep -E 'frappe|nextcloud|mautic|n8n|odoo'" ) - containers = [] - if stdout: - for line in stdout.split('\n'): - if '|' not in line: - continue - parts = line.split('|') - containers.append({ - 'name': parts[0].strip(), - 'status': parts[1].strip(), - 'image': parts[2].strip(), - 'ports': parts[3].strip() if len(parts) > 3 else '', - 'owner': 'root', - }) - return containers + return _parse_containers(stdout) def get_all_root_containers(): - """ALL root docker containers (unfiltered).""" - stdout, _ = _run( + """ALL root docker containers (unfiltered) — always from main server.""" + stdout, _ = _ssh_main( "docker ps -a --format '{{.Names}}|{{.Status}}|{{.Image}}|{{.Ports}}' 2>/dev/null" ) + return _parse_containers(stdout) + + +def get_rootless_user_containers_remote(): + """ + Get containers from all rootless-docker users ON THE MAIN SERVER. + Root reads /run/user/*/docker.sock via SSH. + """ + # List all non-system UIDs that have a docker socket on main server + cmd = "ls /run/user/*/docker.sock 2>/dev/null" + stdout, _ = _ssh_main(cmd) containers = [] - if stdout: - for line in stdout.split('\n'): - if '|' not in line: - continue - parts = line.split('|') - containers.append({ - 'name': parts[0].strip(), - 'status': parts[1].strip(), - 'image': parts[2].strip(), - 'ports': parts[3].strip() if len(parts) > 3 else '', - 'owner': 'root', - }) + if not stdout: + return containers + + for sock_path in stdout.split('\n'): + sock_path = sock_path.strip() + if not sock_path: + continue + # Determine username from uid + try: + uid = sock_path.split('/run/user/')[1].split('/')[0] + except (IndexError, ValueError): + continue + + name_out, _ = _ssh_main(f"getent passwd {uid} | cut -d: -f1") + username = name_out.strip() or f"uid{uid}" + + ctr_out, _ = _ssh_main( + f"DOCKER_HOST=unix://{sock_path} " + f"docker ps -a --format '{{{{.Names}}}}|{{{{.Status}}}}|{{{{.Image}}}}|{{{{.Ports}}}}' 2>/dev/null" + ) + containers.extend(_parse_containers(ctr_out, owner=username)) + return containers # ──────────────────────────────────────────────────────────────── -# CONTAINER STATS +# CONTAINER ACTIONS (start / stop / restart) — on main server # ──────────────────────────────────────────────────────────────── -def get_container_stats(docker_socket=None): - """One-shot stats snapshot. Returns dict keyed by container name.""" - if docker_socket: - cmd = ( - f"DOCKER_HOST=unix://{docker_socket} " - f"docker stats --no-stream --format " - f"'{{{{.Name}}}}|{{{{.CPUPerc}}}}|{{{{.MemUsage}}}}|{{{{.MemPerc}}}}|{{{{.NetIO}}}}|{{{{.BlockIO}}}}' 2>/dev/null" - ) - else: - cmd = ( - "docker stats --no-stream --format " - "'{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}|{{.NetIO}}|{{.BlockIO}}' 2>/dev/null" - ) +def container_action(container_name, action): + """ + action: 'start' | 'stop' | 'restart' + Returns (success: bool, output: str) + """ + if action not in ('start', 'stop', 'restart'): + return False, "Invalid action" - stdout, _ = _run(cmd, timeout=30) + safe_name = container_name.replace('"', '').replace(';', '').replace('|', '') + stdout, stderr = _ssh_main( + f"docker {action} {safe_name} 2>&1", + timeout=30 + ) + output = (stdout + stderr).strip() + success = safe_name in output or 'started' in output.lower() or stderr == '' + return True, output + + +# ──────────────────────────────────────────────────────────────── +# STATS — from main server +# ──────────────────────────────────────────────────────────────── + +def get_container_stats_remote(): + """One-shot stats for all root containers on main server.""" + stdout, _ = _ssh_main( + "docker stats --no-stream --format " + "'{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}|{{.NetIO}}|{{.BlockIO}}' 2>/dev/null", + timeout=35 + ) stats = {} if stdout: for line in stdout.split('\n'): @@ -141,40 +216,63 @@ def get_container_stats(docker_socket=None): def get_all_stats(): - """Stats for root + all rootless-user containers combined.""" - all_stats = get_container_stats() - try: - import pwd - for pw in pwd.getpwall(): - if pw.pw_uid < 1000 or pw.pw_name == 'nobody': + """Stats for root containers on main server + rootless users on main server.""" + all_stats = get_container_stats_remote() + + # Also get stats from rootless user sockets on main server + socks_out, _ = _ssh_main("ls /run/user/*/docker.sock 2>/dev/null") + if socks_out: + for sock in socks_out.split('\n'): + sock = sock.strip() + if not sock: continue - sock = f"/run/user/{pw.pw_uid}/docker.sock" - if os.path.exists(sock): - user_stats = get_container_stats(docker_socket=sock) - all_stats.update(user_stats) - except Exception as e: - print(f"[stats] Error: {e}") + stdout, _ = _ssh_main( + f"DOCKER_HOST=unix://{sock} " + f"docker stats --no-stream --format " + f"'{{{{.Name}}}}|{{{{.CPUPerc}}}}|{{{{.MemUsage}}}}|{{{{.MemPerc}}}}|{{{{.NetIO}}}}|{{{{.BlockIO}}}}' 2>/dev/null", + timeout=35 + ) + if stdout: + for line in stdout.split('\n'): + if '|' not in line: + continue + parts = line.split('|') + if len(parts) < 6: + continue + all_stats[parts[0].strip()] = { + 'cpu': parts[1].strip(), + 'mem': parts[2].strip(), + 'mem_pct': parts[3].strip(), + 'net': parts[4].strip(), + 'block': parts[5].strip(), + } return all_stats +# ──────────────────────────────────────────────────────────────── +# SYSTEM INFO — from main server +# ──────────────────────────────────────────────────────────────── + def get_system_info(): - """Host-level system stats.""" - cpu_out, _ = _run("top -bn1 | grep 'Cpu(s)' | awk '{print $2+$4}'") - mem_out, _ = _run("free -m | awk 'NR==2{printf \"%s/%sMB\", $3, $2}'") - mem_pct, _ = _run("free | awk 'NR==2{printf \"%.0f\", $3/$2*100}'") - disk_out, _ = _run("df -h / | awk 'NR==2{printf \"%s/%s\", $3, $2}'") - disk_pct, _ = _run("df / | awk 'NR==2{print $5}' | tr -d '%'") - load_out, _ = _run("cat /proc/loadavg | awk '{print $1, $2, $3}'") - uptime_out, _ = _run("uptime -p") - docker_v, _ = _run("docker --version | cut -d' ' -f3 | tr -d ','") + """Host-level system stats — always fetched from main server.""" + cpu_out, _ = _ssh_main("top -bn1 | grep 'Cpu(s)' | awk '{print $2+$4}'") + mem_out, _ = _ssh_main("free -m | awk 'NR==2{printf \"%s/%sMB\", $3, $2}'") + mem_pct, _ = _ssh_main("free | awk 'NR==2{printf \"%.0f\", $3/$2*100}'") + disk_out, _ = _ssh_main("df -h / | awk 'NR==2{printf \"%s/%s\", $3, $2}'") + disk_pct, _ = _ssh_main("df / | awk 'NR==2{print $5}' | tr -d '%'") + load_out, _ = _ssh_main("cat /proc/loadavg | awk '{print $1, $2, $3}'") + uptime, _ = _ssh_main("uptime -p") + docker_v, _ = _ssh_main("docker --version | cut -d' ' -f3 | tr -d ','") + hostname, _ = _run("hostname -f 2>/dev/null || hostname") # THIS host, not main server return { - 'cpu_pct': cpu_out or '0', - 'memory': mem_out or 'N/A', - 'mem_pct': mem_pct or '0', + 'cpu_pct': cpu_out or '0', + 'memory': mem_out or 'N/A', + 'mem_pct': mem_pct or '0', 'disk': disk_out or 'N/A', 'disk_pct': disk_pct or '0', 'load': load_out or 'N/A', - 'uptime': uptime_out or 'N/A', + 'uptime': uptime or 'N/A', 'docker_v': docker_v or 'N/A', + 'hostname': hostname or 'this server', } diff --git a/platform/templates/dashboard.html b/platform/templates/dashboard.html index cc9672e..f4b1c3d 100644 --- a/platform/templates/dashboard.html +++ b/platform/templates/dashboard.html @@ -1,12 +1,24 @@ {% extends "base.html" %} {% block content %} +{# Macro must be defined BEFORE it is used #} +{% macro ctr_actions(name) %} + + + +{% endmacro %} + {# ═══════════════════════════════════════════════════════ DASHBOARD PAGE ═══════════════════════════════════════════════════════ #}
-
CPU USAGE
@@ -30,41 +42,29 @@
-
Overview
- Docker {{ system.docker_v }} + Docker {{ system.docker_v }} · {{ main_server }}
-
-
{{ containers|length }}
-
App Containers
-
-
-
{{ running_count }}
-
Running
-
-
-
{{ users|length }}
-
Linux Users
-
-
-
{{ backups|length }}
-
Local Backups
-
-
-
{{ vm_backups|length }}
-
VM Backups
-
+
{{ containers|length }}
App Containers
+
{{ running_count }}
Running
+
{{ users|length }}
Linux Users
+
{{ backups|length }}
Local Backups
+
{{ vm_backups|length }}
VM Backups
-
App Containers
- Auto-refresh every 15s +
+ Auto-refresh 15s + +
@@ -75,14 +75,15 @@ - - - + + + + {% for c in containers %} - + - - - + + + + {% else %} - + {% endfor %}
CPU MEMORY NET I/ODISK I/OIMAGEPORTSACTIONS
{{ c.name }} {% if 'Up' in c.status %} @@ -94,19 +95,18 @@
-
-
-
+
{{ c.image }}{{ c.ports or '—' }}
{{ ctr_actions(c.name) }}
No containers found
No containers
@@ -115,14 +115,17 @@
{# ═══════════════════════════════════════════════════════ - ALL CONTAINERS PAGE (root + all users) + ALL CONTAINERS PAGE ═══════════════════════════════════════════════════════ #}
All Containers
-
- Loading… +
+ +
@@ -136,12 +139,14 @@ CPU MEMORY NET I/O - IMAGE - PORTS + DISK I/O + IMAGE + PORTS + ACTIONS -
Loading…
+
Loading…
@@ -158,8 +163,6 @@
- -
STEP 1 — SELECT BACKUP SOURCE
@@ -184,29 +187,19 @@
-
-
STEP 2 — SELECT RESTORE TARGET
@@ -215,8 +208,8 @@
🎯
-
This Server
-
{{ main_server }}
+
Restore on This Server
+
Loading hostname…
@@ -225,8 +218,8 @@
📡
-
Remote Machine
-
via SSH
+
External Machine
+
via SSH — any IP
@@ -236,18 +229,17 @@
- +
- +
-
@@ -261,37 +253,32 @@
-
-
-

- ⚠ Healthy running containers will be skipped automatically. -

+

⚠ Healthy running containers are skipped.

-
@@ -313,10 +300,7 @@
{% for b in backups %} -
- {{ b }} - -
+
{{ b }}
{% else %}
No backups
{% endfor %} @@ -329,10 +313,7 @@
{% for b in vm_backups %} -
- {{ b }} - -
+
{{ b }}
{% else %}
No VM backups
{% endfor %} @@ -346,8 +327,6 @@ USERS PAGE ═══════════════════════════════════════════════════════ #}
- -
Create New User
@@ -359,10 +338,10 @@
- +
- +
@@ -370,16 +349,13 @@ Setup rootless Docker - +
-
System Users
@@ -389,52 +365,40 @@ {% for u in users %}
-
- {{ u.name[0].upper() }} -
+
{{ u.name[0].upper() }}
{{ u.name }}
-
uid {{ u.uid }} · {{ u.home }}
+
uid {{ u.uid }}
- {% if u.has_docker %} rootless docker{% endif %} - {% if u.linger %}linger on{% endif %} + {% if u.has_docker %} docker{% endif %} + {% if u.linger %}linger{% endif %} + {% if u.has_vdisk %}💾 vdisk{% endif %}
Disk: {{ u.disk_used }}
-
Containers: {{ u.container_count }}
+
Ctrs: {{ u.container_count }}
{% else %} -
- No non-system users found -
+
No non-system users
{% endfor %}
-