Sync from main server - 2026-05-05 00:20:15

This commit is contained in:
root
2026-05-05 00:20:15 +02:00
parent a8db6b5fa2
commit 09bbe0403c
5 changed files with 560 additions and 336 deletions

View File

@@ -30,8 +30,11 @@ from modules.users import (
app = Flask(__name__)
app.secret_key = 'navitrends-secret-key-2025'
restore_jobs = {}
backup_jobs = {} # for manual backup runs
# Increase default timeout for slow VM→main-server SSH calls
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
restore_jobs = {}
backup_jobs = {}
def _stream_restore(job_id, cmd):
@@ -53,7 +56,6 @@ def _stream_restore(job_id, cmd):
def _stream_backup(job_id, script_path):
"""Run the backup script and stream its output into backup_jobs."""
backup_jobs[job_id] = {'status': 'running', 'log': [], 'started': time.time()}
try:
proc = subprocess.Popen(
@@ -73,16 +75,27 @@ def _stream_backup(job_id, script_path):
# ─────────────────────────────────────────────
# DASHBOARD
# Loads instantly — all heavy data fetched async via JS after page renders
# ─────────────────────────────────────────────
@app.route('/')
@login_required
def dashboard():
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()
# On the VM: skip slow SSH calls at page load — JS fetches them async via /api/dashboard
# On the main server: fetch everything normally (local calls, no SSH delay)
backups = get_local_backups()
vm_backups = get_vm_backups()
if RUNNING_ON_MAIN_SERVER:
containers = get_containers()
running_count = sum(1 for c in containers if 'Up' in c.get('status', ''))
system = get_system_info()
users = get_all_users()
else:
containers = [] # loaded async by JS via /api/dashboard
running_count = 0
system = {}
users = []
return render_template('pages/dashboard.html',
containers=containers,
running_count=running_count,
@@ -145,9 +158,12 @@ def restore_page():
@app.route('/users')
@login_required
def users_page():
# On VM: skip slow SSH call — JS loads users async via /api/users
# On main server: fetch normally (local, fast)
users = get_all_users() if RUNNING_ON_MAIN_SERVER else []
return render_template(
'pages/users.html',
users=get_all_users(),
users=users,
main_server=MAIN_SERVER_IP,
active_page='users',
page_title='User Management',
@@ -158,10 +174,13 @@ def users_page():
@app.route('/settings')
@login_required
def settings_page():
# On VM: skip slow SSH call — JS loads system info async via /api/system
# On main server: fetch normally (local, fast)
system = get_system_info() if RUNNING_ON_MAIN_SERVER else {}
return render_template(
'pages/settings.html',
main_server=MAIN_SERVER_IP,
system=get_system_info(),
system=system,
running_on_main=RUNNING_ON_MAIN_SERVER,
active_page='settings',
page_title='Settings',
@@ -196,24 +215,51 @@ def api_containers():
@login_required
def api_containers_all():
"""Root containers + rootless-user containers, all from main server."""
root_ctrs = get_all_root_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', ''))
root_ctrs = get_all_root_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})
@app.route('/api/nav-summary')
@login_required
def api_nav_summary():
"""Lightweight counts for sidebar badges on every page (one round trip)."""
"""Lightweight counts for sidebar badges (one round trip)."""
root_ctrs = get_all_root_containers()
user_ctrs = get_rootless_user_containers_remote()
all_ctrs = root_ctrs + user_ctrs
users = get_all_users()
all_ctrs = root_ctrs + user_ctrs
users = get_all_users()
return jsonify({
'container_count': len(all_ctrs),
'user_count': len(users),
'user_count': len(users),
})
# ─────────────────────────────────────────────
# API — dashboard summary (fast async load)
# ─────────────────────────────────────────────
@app.route('/api/dashboard')
@login_required
def api_dashboard():
"""
Single endpoint the dashboard JS calls after page render.
Returns system info + container summary + user count in one shot.
"""
system = get_system_info()
root_ctrs = get_all_root_containers()
user_ctrs = get_rootless_user_containers_remote()
all_ctrs = root_ctrs + user_ctrs
users = get_all_users()
running = sum(1 for c in all_ctrs if 'Up' in c.get('status', ''))
return jsonify({
'system': system,
'containers': all_ctrs,
'running_count': running,
'user_count': len(users),
'local_backups': len(get_local_backups()),
'vm_backups': len(get_vm_backups()),
})
@@ -223,11 +269,6 @@ def api_nav_summary():
@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, then immediately returns the NEW container status so the
UI can update without waiting for the next 15-second refresh cycle.
"""
data = request.get_json() or {}
name = data.get('name', '').strip()
action = data.get('action', '').strip()
@@ -236,26 +277,20 @@ def api_container_action():
return jsonify({'success': False, 'message': 'name and action required'}), 400
success, output = container_action(name, action)
# Give Docker a moment to settle, then fetch the real status
time.sleep(1.5)
status_info = get_container_status(name)
return jsonify({
'success': success,
'output': output,
'new_status': status_info['status'], # 'running' | 'stopped' | 'unknown'
'success': success,
'output': output,
'new_status': status_info['status'],
'new_status_raw': status_info['raw'],
})
# ─────────────────────────────────────────────
# API — single container status (for polling)
# ─────────────────────────────────────────────
@app.route('/api/container/status/<name>')
@login_required
def api_container_status(name):
"""Quick single-container status check."""
status_info = get_container_status(name)
return jsonify(status_info)
@@ -272,22 +307,14 @@ def api_backups():
@app.route('/api/backups/log')
@login_required
def api_backup_log():
"""Return the last N backup log entries."""
limit = int(request.args.get('limit', 20))
entries = get_backup_log_entries(limit)
return jsonify({'entries': entries})
# ─────────────────────────────────────────────
# API — backup health audit
# ─────────────────────────────────────────────
@app.route('/api/backups/audit', methods=['POST'])
@login_required
def api_backup_audit():
"""
POST JSON: { "backup_file": "myapps-backup-…tar.gz", "source": "local"|"vm" }
Returns full audit report.
"""
data = request.get_json() or {}
bfile = data.get('backup_file', '').strip()
source = data.get('source', 'local').strip()
@@ -299,15 +326,9 @@ def api_backup_audit():
return jsonify(result)
# ─────────────────────────────────────────────
# API — delete backup
# ─────────────────────────────────────────────
@app.route('/api/backups/delete', methods=['POST'])
@login_required
def api_backup_delete():
"""
POST JSON: { "backup_file": "myapps-backup-…tar.gz", "source": "local"|"vm" }
"""
data = request.get_json() or {}
bfile = data.get('backup_file', '').strip()
source = data.get('source', 'local').strip()
@@ -319,17 +340,9 @@ def api_backup_delete():
return jsonify({'success': success, 'message': message})
# ─────────────────────────────────────────────
# API — manual backup trigger
# ─────────────────────────────────────────────
@app.route('/api/backups/run', methods=['POST'])
@login_required
def api_backup_run():
"""
Trigger a manual backup run on the main server.
Returns a job_id so the UI can poll /api/backups/run/status/<job_id>.
Only works when running on the main server (where the backup script lives).
"""
if not RUNNING_ON_MAIN_SERVER:
return jsonify({
'success': False,
@@ -352,7 +365,6 @@ def api_backup_run():
@app.route('/api/backups/run/status/<job_id>')
@login_required
def api_backup_run_status(job_id):
"""Poll manual backup job status."""
job = backup_jobs.get(job_id)
if not job:
return jsonify({'error': 'Job not found'}), 404
@@ -364,7 +376,7 @@ def api_backup_run_status(job_id):
# ─────────────────────────────────────────────
# API — users (LOCAL — users on this host)
# API — users
# ─────────────────────────────────────────────
@app.route('/api/users')
@login_required
@@ -442,7 +454,6 @@ def restore_start():
if not backup_file:
return jsonify({'error': 'No backup file specified'}), 400
# ── Resolve backup archive path ──────────────────────────────────────────
if backup_source == 'local':
if RUNNING_ON_MAIN_SERVER:
backup_path = f"/root/backups/{backup_file}"
@@ -498,7 +509,6 @@ def restore_start():
f"cd {session_dir} && bash restore-myapps.sh ; "
f"EXIT=$? ; rm -rf {session_dir} ; exit $EXIT"
)
else:
if not remote_ip:
return jsonify({'error': 'remote_ip required'}), 400
@@ -586,4 +596,4 @@ def logout():
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
app.run(host='0.0.0.0', port=5000, debug=False)