diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index 2c6f8ea1..e5390cd7 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -2432,9 +2432,30 @@ def api_storage_summary(): return jsonify(storage_data) except Exception as e: return jsonify({'error': str(e)}), 500 -# END OF CHANGE FOR /api/storage/summary + # END OF CHANGE FOR /api/storage/summary -def get_interface_type(interface_name): + @app.route('/api/storage/observations', methods=['GET']) + @require_auth + def api_storage_observations(): + """Get disk observations (permanent error history) for a specific disk or all disks.""" + try: + device = request.args.get('device', '') + serial = request.args.get('serial', '') + + # Strip /dev/ prefix if present + if device.startswith('/dev/'): + device = device[5:] + + observations = health_persistence.get_disk_observations( + device_name=device or None, + serial=serial or None + ) + + return jsonify({'observations': observations}) + except Exception as e: + return jsonify({'observations': [], 'error': str(e)}), 500 + + def get_interface_type(interface_name): """Detect the type of network interface""" try: # Skip loopback @@ -6515,6 +6536,21 @@ def api_health(): 'version': '1.0.2' }) +@app.route('/api/health/acknowledge', methods=['POST']) +@require_auth +def api_health_acknowledge(): + """Acknowledge/dismiss a health error by error_key.""" + try: + data = request.get_json() + error_key = data.get('error_key', '') + if not error_key: + return jsonify({'error': 'error_key is required'}), 400 + + result = health_persistence.acknowledge_error(error_key) + return jsonify({'success': True, 'result': result}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + @app.route('/api/prometheus', methods=['GET']) @require_auth def api_prometheus(): diff --git a/AppImage/scripts/health_monitor.py b/AppImage/scripts/health_monitor.py index d38986d7..9c32b8b1 100644 --- a/AppImage/scripts/health_monitor.py +++ b/AppImage/scripts/health_monitor.py @@ -973,10 +973,10 @@ class HealthMonitor: zfs_error_key = f'zfs_pool_{real_pool}' zfs_reason = f'ZFS pool {real_pool}: {pool_info["reason"]}' try: - if not health_persistence.is_error_active(zfs_error_key, category='zfs'): + if not health_persistence.is_error_active(zfs_error_key, category='disks'): health_persistence.record_error( error_key=zfs_error_key, - category='zfs', + category='disks', severity=pool_info.get('status', 'WARNING'), reason=zfs_reason, details={ @@ -1102,7 +1102,12 @@ class HealthMonitor: for sl in (issue.get('smart_lines') or [])[:3]: smart_details_parts.append(sl) detail_text = '; '.join(smart_details_parts[:3]) if smart_details_parts else 'SMART warning in journal' - checks['smart_health'] = {'status': 'WARNING', 'detail': detail_text} + checks['smart_health'] = { + 'status': 'WARNING', + 'detail': detail_text, + 'dismissable': True, + 'error_key': 'smart_health_journal', + } else: checks['smart_health'] = {'status': 'OK', 'detail': 'No SMART warnings in journal'} if self.capabilities.get('has_zfs') and 'zfs_pools' not in checks: @@ -3907,6 +3912,8 @@ class HealthMonitor: 'io_lines': unique_io[:5], 'sample': sample_line, 'source': 'journal', + 'dismissable': True, + 'error_key': f'smart_{disk_name}', } # Record as disk observation for the permanent history diff --git a/AppImage/scripts/health_persistence.py b/AppImage/scripts/health_persistence.py index ec2adfde..937ce5ab 100644 --- a/AppImage/scripts/health_persistence.py +++ b/AppImage/scripts/health_persistence.py @@ -480,7 +480,8 @@ class HealthPersistence: ('updates', 'pending_updates'), ('updates', 'kernel_pve'), ('security', 'security_'), ('pve_services', 'pve_service_'), ('vms', 'vmct_'), ('vms', 'vm_'), ('vms', 'ct_'), - ('disks', 'disk_'), ('logs', 'log_'), ('network', 'net_'), + ('disks', 'disk_'), ('disks', 'smart_'), ('disks', 'zfs_pool_'), + ('logs', 'log_'), ('network', 'net_'), ('temperature', 'temp_')]: if error_key == prefix or error_key.startswith(prefix): category = cat