Remove the notification menu from the VM and LXC backup dialog; backups follow the notification settings

This commit is contained in:
MacRimi
2026-10-18 17:43:41 +02:00
parent 700a4de2ee
commit 947b5291e9
11 changed files with 77 additions and 98 deletions
+16 -5
View File
@@ -560,10 +560,12 @@ def totp_setup():
"""Initialize TOTP setup for a user"""
try:
token = request.headers.get('Authorization', '').replace('Bearer ', '')
username = auth_manager.verify_token(token)
username, scope = auth_manager.verify_token_full(token)
if not username:
return jsonify({"success": False, "message": "Unauthorized"}), 401
if scope != 'full_admin':
return jsonify({"success": False, "message": "This action requires a full_admin token"}), 403
success, secret, qr_code, backup_codes, message = auth_manager.setup_totp(username)
@@ -586,10 +588,12 @@ def totp_enable():
"""Enable TOTP after verification"""
try:
token = request.headers.get('Authorization', '').replace('Bearer ', '')
username = auth_manager.verify_token(token)
username, scope = auth_manager.verify_token_full(token)
if not username:
return jsonify({"success": False, "message": "Unauthorized"}), 401
if scope != 'full_admin':
return jsonify({"success": False, "message": "This action requires a full_admin token"}), 403
data = request.json
verification_token = data.get('token')
@@ -612,10 +616,12 @@ def totp_disable():
"""Disable TOTP (requires password confirmation)"""
try:
token = request.headers.get('Authorization', '').replace('Bearer ', '')
username = auth_manager.verify_token(token)
username, scope = auth_manager.verify_token_full(token)
if not username:
return jsonify({"success": False, "message": "Unauthorized"}), 401
if scope != 'full_admin':
return jsonify({"success": False, "message": "This action requires a full_admin token"}), 403
data = request.json or {}
password = data.get('password')
@@ -653,10 +659,12 @@ def generate_api_token():
if not token:
return jsonify({"success": False, "message": "Unauthorized. Please log in first."}), 401
username = auth_manager.verify_token(token)
username, scope = auth_manager.verify_token_full(token)
if not username:
return jsonify({"success": False, "message": "Invalid or expired session. Please log in again."}), 401
if scope != 'full_admin':
return jsonify({"success": False, "message": "This action requires a full_admin token"}), 403
data = request.json
password = data.get('password')
@@ -749,8 +757,11 @@ def revoke_api_token_route(token_id):
return jsonify({"success": False, "message": "Authentication is not configured"}), 400
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token or not auth_manager.verify_token(token):
username, scope = auth_manager.verify_token_full(token) if token else (None, None)
if not username:
return jsonify({"success": False, "message": "Unauthorized"}), 401
if scope != 'full_admin':
return jsonify({"success": False, "message": "This action requires a full_admin token"}), 403
success, message = auth_manager.revoke_api_token(token_id)
+18 -4
View File
@@ -4549,6 +4549,8 @@ def get_storage_info():
disk_info['used'] = round(partition_usage.used / (1024**3), 1)
disk_info['available'] = round(partition_usage.free / (1024**3), 1)
disk_info['usage_percent'] = round(partition_usage.percent, 1)
disk_info['total_bytes'] = partition_usage.total
disk_info['used_bytes'] = partition_usage.used
except PermissionError:
continue
@@ -13095,7 +13097,6 @@ def api_create_backup(vmid):
mode = data.get('mode', 'snapshot') # snapshot, suspend, stop
compress = data.get('compress', 'zstd') # none, lzo, gzip, zstd
protected = data.get('protected', False) # True/False
notification = data.get('notification', 'auto') # always, failure, never, auto
notes = data.get('notes', '') # Backup notes/description
pbs_change_detection = data.get('pbs_change_detection', None) # default, legacy, data (for PBS + LXC)
@@ -13614,6 +13615,19 @@ def get_task_log(upid):
traceback.print_exc()
return jsonify({'error': str(e)}), 500
def _installed_proxmenux_version():
"""Installed ProxMenux version; a beta install keeps beta_version.txt."""
for name in ('beta_version.txt', 'version.txt'):
try:
with open(f'/usr/local/share/proxmenux/{name}') as fh:
version = fh.read().strip()
if version:
return version
except OSError:
continue
return ''
@app.route('/api/health', methods=['GET'])
@require_auth
def api_health():
@@ -13621,7 +13635,7 @@ def api_health():
return jsonify({
'status': 'healthy',
'timestamp': datetime.now().isoformat(),
'version': '1.2.2'
'version': _installed_proxmenux_version()
})
# ─── User-configurable health thresholds ─────────────────────────────────────
@@ -15047,11 +15061,11 @@ def api_prometheus():
disk_name = disk.get('name', 'unknown')
metrics.append(f'# HELP proxmox_disk_total_bytes Total disk space in bytes')
metrics.append(f'# TYPE proxmox_disk_total_bytes gauge')
metrics.append(f'proxmox_disk_total_bytes{{node="{node}",disk="{disk_name}"}} {disk.get("total", 0)} {timestamp}')
metrics.append(f'proxmox_disk_total_bytes{{node="{node}",disk="{disk_name}"}} {disk.get("total_bytes", 0)} {timestamp}')
metrics.append(f'# HELP proxmox_disk_used_bytes Used disk space in bytes')
metrics.append(f'# TYPE proxmox_disk_used_bytes gauge')
metrics.append(f'proxmox_disk_used_bytes{{node="{node}",disk="{disk_name}"}} {disk.get("used", 0)} {timestamp}')
metrics.append(f'proxmox_disk_used_bytes{{node="{node}",disk="{disk_name}"}} {disk.get("used_bytes", 0)} {timestamp}')
metrics.append(f'# HELP proxmox_disk_usage_percent Disk usage percentage')
metrics.append(f'# TYPE proxmox_disk_usage_percent gauge')
+3 -3
View File
@@ -1637,7 +1637,7 @@ class HealthMonitor:
mem_critical_samples = [
entry for entry in self.state_history[state_key]
if entry['mem_percent'] >= 90 and
if entry['mem_percent'] >= self.MEMORY_CRITICAL and
current_time - entry['time'] <= self.MEMORY_DURATION
]
@@ -1671,7 +1671,7 @@ class HealthMonitor:
oldest = min(s['time'] for s in mem_critical_samples)
actual_duration = int(current_time - oldest)
status = 'CRITICAL'
reason = f'RAM >90% sustained for {actual_duration}s'
reason = f'RAM >{self.MEMORY_CRITICAL}% sustained for {actual_duration}s'
elif swap_critical >= 2:
status = 'CRITICAL'
reason = (f'Memory pressure: swap {swap_percent:.0f}% used '
@@ -1691,7 +1691,7 @@ class HealthMonitor:
swap_total_gb = round(swap.total / (1024**3), 2)
# Determine per-sub-check status
ram_status = 'CRITICAL' if mem_percent >= 90 and mem_critical_count >= MEM_CRITICAL_MIN_SAMPLES else ('WARNING' if mem_percent >= self.MEMORY_WARNING and mem_warning_count >= MEM_WARNING_MIN_SAMPLES else 'OK')
ram_status = 'CRITICAL' if mem_percent >= self.MEMORY_CRITICAL and mem_critical_count >= MEM_CRITICAL_MIN_SAMPLES else ('WARNING' if mem_percent >= self.MEMORY_WARNING and mem_warning_count >= MEM_WARNING_MIN_SAMPLES else 'OK')
swap_status = 'CRITICAL' if swap_critical >= 2 else 'OK'
result = {
+9 -1
View File
@@ -8,6 +8,8 @@ from flask import request, jsonify, g
from functools import wraps
from auth_manager import load_auth_config, verify_token, verify_token_full
_READ_METHODS = ('GET', 'HEAD', 'OPTIONS')
def require_auth(f):
"""
@@ -64,12 +66,18 @@ def require_auth(f):
token = parts[1]
# Verify token
username = verify_token(token)
username, scope = verify_token_full(token)
if not username:
return jsonify({
"error": "Invalid or expired token",
"message": "Please log in again"
}), 401
# read_only tokens may only read; any method that can change
# state needs full_admin.
if request.method not in _READ_METHODS and scope != 'full_admin':
return jsonify({"error": "Insufficient scope",
"message": f"This action requires a full_admin token (your token: {scope})"}), 403
# Token is valid, allow access
return f(*args, **kwargs)
+3 -1
View File
@@ -3335,6 +3335,7 @@ def _ai_cache_key(title, body, ai_config, detail_level, use_emojis):
str(ai_config.get('ai_provider', '')), '\x1f',
str(ai_config.get('ai_model', '')), '\x1f',
str(ai_config.get('ai_language', '')), '\x1f',
str(ai_config.get('channel_type', '')).lower(), '\x1f',
detail_level, '\x1f', '1' if use_emojis else '0',
]
return _hash_ai_cache.sha256(''.join(parts).encode('utf-8', 'replace')).hexdigest()
@@ -3428,7 +3429,8 @@ def format_with_ai_full(title: str, body: str, severity: str,
channel_type = ai_config.get('channel_type', '').lower()
is_email = channel_type == 'email'
if is_email and detail_level == 'detailed' and body and len(body) > 50:
if (is_email and detail_level == 'detailed' and body and len(body) > 50
and result_body != body):
# Only append if original has substantial content
result_body += "\n\n" + "-" * 40 + "\n"
result_body += "Original message:\n"