mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 02:42:08 +00:00
fix: refresh reboot evidence after external host updates
This commit is contained in:
@@ -243,10 +243,14 @@ def get_full_health():
|
||||
security before returning, so an event that just changed underlying state
|
||||
(Update Now finished, dismiss action) sees the new value immediately
|
||||
instead of waiting for the next polling tick.
|
||||
?cached=1 is a strictly read-only poll: no full checks or cache clearing,
|
||||
even with refresh=1. Unavailable/stale snapshots return 503. Every valid
|
||||
response refreshes reboot evidence, while keeping the full audit's date.
|
||||
"""
|
||||
import time as _time
|
||||
try:
|
||||
if request.args.get('refresh') == '1':
|
||||
cached_only = request.args.get('cached') == '1'
|
||||
if not cached_only and request.args.get('refresh') == '1':
|
||||
for ck in ('updates_check', 'pve_services', 'security_check',
|
||||
'_bg_detailed', '_bg_overall', 'overall_health'):
|
||||
health_monitor.last_check_times.pop(ck, None)
|
||||
@@ -255,14 +259,24 @@ def get_full_health():
|
||||
# Try to use the background-cached detailed result for instant response
|
||||
bg_key = '_bg_detailed'
|
||||
bg_last = health_monitor.last_check_times.get(bg_key, 0)
|
||||
bg_age = _time.time() - bg_last
|
||||
bg_age = _time.time() - bg_last if type(bg_last) in (int, float) else float('inf')
|
||||
|
||||
if bg_age < 360 and bg_key in health_monitor.cached_results:
|
||||
if 0 <= bg_age < 360 and bg_key in health_monitor.cached_results:
|
||||
# Use cached result (at most ~5 min old)
|
||||
details = health_monitor.cached_results[bg_key]
|
||||
elif cached_only:
|
||||
# Automatic polling must never start APT or a full audit, even
|
||||
# during startup, collector failure, expiry or a conflicting flag.
|
||||
return jsonify({'error': 'Health snapshot is not available yet'}), 503
|
||||
else:
|
||||
# No fresh cache, run live (first load or cache expired)
|
||||
details = health_monitor.get_detailed_status()
|
||||
|
||||
# Installed/running kernel state changes outside Monitor jobs too.
|
||||
# Overlay fresh read-only evidence without refreshing other checks or
|
||||
# rewriting the background collector's snapshot/timestamp.
|
||||
from kernel_reboot import refresh_health_reboot
|
||||
details = refresh_health_reboot(details)
|
||||
|
||||
active_errors = health_persistence.get_active_errors()
|
||||
dismissed = health_persistence.get_dismissed_errors()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Read-only reboot evidence, independent of APT pending-update caches."""
|
||||
import os
|
||||
import copy
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
@@ -63,6 +64,13 @@ def with_reboot_status(updates, reboot):
|
||||
return updates
|
||||
result = dict(updates or dict(status='UNKNOWN', count=None, checks={}))
|
||||
result['checks'] = dict(result.get('checks') or {})
|
||||
# Keep the package result separate so a later reboot-only overlay can
|
||||
# clear its own INFO/UNKNOWN without hiding a package-check failure.
|
||||
base_status = result.get('package_status', result.get('status', 'UNKNOWN'))
|
||||
if result.get('all_dismissed'):
|
||||
base_status = 'OK'
|
||||
result['package_status'] = base_status
|
||||
result['status'] = base_status
|
||||
required = reboot['state'] == 'required'
|
||||
unknown = reboot['state'] == 'unknown'
|
||||
result['reboot_required'] = True if required else None if unknown else False
|
||||
@@ -77,3 +85,32 @@ def with_reboot_status(updates, reboot):
|
||||
if result.get('status') == 'OK' and (required or unknown):
|
||||
result['status'] = 'INFO' if required else 'UNKNOWN'
|
||||
return result
|
||||
|
||||
|
||||
def refresh_health_reboot(snapshot):
|
||||
"""Refresh only reboot evidence on an immutable, dated health snapshot.
|
||||
|
||||
Never runs the full health check, APT, persistence writes or lifecycle
|
||||
commands. Other categories and the audit timestamp retain their age.
|
||||
"""
|
||||
result = copy.deepcopy(snapshot)
|
||||
details = result.get('details')
|
||||
if not isinstance(details, dict) or not isinstance(details.get('updates'), dict):
|
||||
raise ValueError('Health snapshot has no updates result')
|
||||
details['updates'] = with_reboot_status(details['updates'], reboot_status())
|
||||
if details['updates']['status'] == snapshot['details']['updates']['status']:
|
||||
return result
|
||||
issues = {status: [] for status in ('CRITICAL', 'WARNING', 'UNKNOWN', 'INFO')}
|
||||
for key, block in details.items():
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
status = block.get('status', 'UNKNOWN')
|
||||
if status in issues:
|
||||
label = key.replace('_', ' ').capitalize()
|
||||
reason = block.get('reason')
|
||||
issues[status].append(f'{label}: {reason}' if reason else label)
|
||||
result['overall'] = ('CRITICAL' if issues['CRITICAL'] else 'WARNING'
|
||||
if issues['WARNING'] or issues['UNKNOWN'] else 'OK')
|
||||
primary = next((values for values in issues.values() if values), [])
|
||||
result['summary'] = '; '.join(primary) if primary else 'All systems operational'
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user