New version 1.2.5

Stable release consolidating the v1.2.4 beta cycle (1.2.4.1-beta and 1.2.4.2-beta) into 1.2.5.

Highlights:

- Apps dashboard: single launcher for every LXC-registered app and user-defined Custom Web Link, with category badges, search, sort and one-click deep-links back to the guest modal.
- LXC Apps & Updates end-to-end: App tab inside every guest modal, upstream version tracking, and Easy Updates that cover OS packages, registered apps, Docker Engine and per-image updates on the same 24-hour cycle.
- Application detection catalog with 380+ tracked workloads generated live from community-scripts across seven detector methods.
- Monitor now speaks 8 languages: English, Spanish, German, French, Italian, Portuguese, Slovak and Swedish (i18n scaffolding by @vaso73).
- NVIDIA multi-GPU passthrough by exact BDF so one card can be assigned to a VM while another stays operational on the host or LXC.
- Navigation reorder, Memory & Swap real memory-pressure signal, native Pushover channel, Actions API, plus wide-reaching improvements across health, hardware, network, backup and post-install.

Full release notes: see CHANGELOG.md and https://github.com/MacRimi/ProxMenux/releases
This commit is contained in:
MacRimi
2026-09-01 19:15:42 +02:00
parent f8e65cc4c4
commit 315259f5ec
54 changed files with 4343 additions and 333 deletions
+41 -18
View File
@@ -219,7 +219,12 @@ class HealthMonitor:
MEMORY_CRITICAL = 95
MEMORY_DURATION = 300 # 5 minutes sustained (aligned with CPU)
SWAP_WARNING_DURATION = 300
SWAP_CRITICAL_PERCENT = 5
# Swap CRITICAL now requires BOTH: swap file nearly full AND RAM
# genuinely tight. Alerting on just one of them fired constantly on
# healthy Proxmox hosts where the kernel proactively swaps out
# inactive pages while RAM remains plentifully available.
SWAP_HIGH_PERCENT = 80 # % of swap file in use
AVAILABLE_MIN_PERCENT = 15 # % of RAM that must stay available
SWAP_CRITICAL_DURATION = 120
# Storage Thresholds
@@ -435,7 +440,8 @@ class HealthMonitor:
(("cpu", "critical"), "CPU_CRITICAL"),
(("memory", "warning"), "MEMORY_WARNING"),
(("memory", "critical"), "MEMORY_CRITICAL"),
(("memory", "swap_critical"), "SWAP_CRITICAL_PERCENT"),
(("memory", "swap_high"), "SWAP_HIGH_PERCENT"),
(("memory", "available_min"), "AVAILABLE_MIN_PERCENT"),
(("host_storage", "warning"), "STORAGE_WARNING"),
(("host_storage", "critical"), "STORAGE_CRITICAL"),
(("cpu_temperature", "warning"), "TEMP_WARNING"),
@@ -625,12 +631,12 @@ class HealthMonitor:
current_time = time.time()
mem_percent = memory.percent
swap_percent = swap.percent if swap.total > 0 else 0
swap_vs_ram = (swap.used / memory.total * 100) if memory.total > 0 else 0
available_percent = (memory.available / memory.total * 100) if memory.total > 0 else 100
state_key = 'memory_usage'
self.state_history[state_key].append({
'mem_percent': mem_percent,
'swap_percent': swap_percent,
'swap_vs_ram': swap_vs_ram,
'available_percent': available_percent,
'time': current_time
})
# Prune entries older than 10 minutes
@@ -1605,30 +1611,36 @@ class HealthMonitor:
def _check_memory_comprehensive(self) -> Dict[str, Any]:
"""
Check memory including RAM and swap with realistic thresholds.
Only alerts on truly problematic memory situations.
Swap CRITICAL requires the memory-pressure AND-clause: swap is
called out only when the swap file is nearly full AND RAM is
genuinely tight (available memory below the configured floor).
Alerting on swap size alone fires constantly on healthy hosts
where Linux proactively swaps out inactive pages — the user
can't act on that signal and it drowns real pressure events.
"""
try:
memory = psutil.virtual_memory()
swap = psutil.swap_memory()
current_time = time.time()
mem_percent = memory.percent
swap_percent = swap.percent if swap.total > 0 else 0
swap_vs_ram = (swap.used / memory.total * 100) if memory.total > 0 else 0
available_percent = (memory.available / memory.total * 100) if memory.total > 0 else 100
state_key = 'memory_usage'
self.state_history[state_key].append({
'mem_percent': mem_percent,
'swap_percent': swap_percent,
'swap_vs_ram': swap_vs_ram,
'available_percent': available_percent,
'time': current_time
})
self.state_history[state_key] = [
entry for entry in self.state_history[state_key]
if current_time - entry['time'] < 600
]
mem_critical_samples = [
entry for entry in self.state_history[state_key]
if entry['mem_percent'] >= 90 and
@@ -1641,10 +1653,15 @@ class HealthMonitor:
current_time - entry['time'] <= self.MEMORY_DURATION
]
# Swap CRITICAL requires BOTH conditions sustained. Older
# samples predating the new `available_percent` field are
# skipped rather than defaulted to a passing value so the
# transition period never manufactures a false positive.
swap_critical = sum(
1 for entry in self.state_history[state_key]
if entry['swap_vs_ram'] > 20 and
current_time - entry['time'] <= self.SWAP_CRITICAL_DURATION
if entry['swap_percent'] > self.SWAP_HIGH_PERCENT
and entry.get('available_percent', 100) < self.AVAILABLE_MIN_PERCENT
and current_time - entry['time'] <= self.SWAP_CRITICAL_DURATION
)
# Require sustained high usage across most of the 300s window.
@@ -1663,7 +1680,8 @@ class HealthMonitor:
reason = f'RAM >90% sustained for {actual_duration}s'
elif swap_critical >= 2:
status = 'CRITICAL'
reason = f'Swap >20% of RAM ({swap_vs_ram:.1f}%)'
reason = (f'Memory pressure: swap {swap_percent:.0f}% used '
f'and only {available_percent:.0f}% RAM available')
elif mem_warning_count >= MEM_WARNING_MIN_SAMPLES:
oldest = min(s['time'] for s in mem_warning_samples)
actual_duration = int(current_time - oldest)
@@ -1672,12 +1690,12 @@ class HealthMonitor:
else:
status = 'OK'
reason = None
ram_avail_gb = round(memory.available / (1024**3), 2)
ram_total_gb = round(memory.total / (1024**3), 2)
swap_used_gb = round(swap.used / (1024**3), 2)
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')
swap_status = 'CRITICAL' if swap_critical >= 2 else 'OK'
@@ -1691,11 +1709,16 @@ class HealthMonitor:
'checks': {
'ram_usage': {
'status': ram_status,
'detail': 'High RAM usage sustained' if ram_status != 'OK' else 'Normal'
'detail': 'High RAM usage sustained' if ram_status != 'OK' else 'Normal',
'dismissable': True,
},
'swap_usage': {
'status': swap_status,
'detail': 'Excessive swap usage' if swap_status != 'OK' else ('Normal' if swap.total > 0 else 'No swap configured')
'detail': (
'Swap nearly full with RAM tight' if swap_status != 'OK'
else ('Normal' if swap.total > 0 else 'No swap configured')
),
'dismissable': True,
}
}
}