mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 10:52:22 +00:00
Remove the notification menu from the VM and LXC backup dialog; backups follow the notification settings
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user