From 876194cdc8253cd4a9d0e1a381002484926b461c Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 19 Mar 2026 19:07:26 +0100 Subject: [PATCH] update storage settings --- AppImage/components/notification-settings.tsx | 89 +++++---- AppImage/components/settings.tsx | 184 +++++++++++++++++- AppImage/components/storage-overview.tsx | 43 +++- AppImage/scripts/flask_health_routes.py | 130 +++++++++++++ AppImage/scripts/flask_notification_routes.py | 7 +- AppImage/scripts/flask_server.py | 18 ++ AppImage/scripts/health_monitor.py | 64 +++++- AppImage/scripts/health_persistence.py | 183 +++++++++++++++++ AppImage/scripts/notification_manager.py | 13 ++ 9 files changed, 668 insertions(+), 63 deletions(-) diff --git a/AppImage/components/notification-settings.tsx b/AppImage/components/notification-settings.tsx index e40d1fd7..0e127006 100644 --- a/AppImage/components/notification-settings.tsx +++ b/AppImage/components/notification-settings.tsx @@ -1426,13 +1426,34 @@ export function NotificationSettings() { {config.ai_provider === "ollama" && (
- updateConfig(p => ({ ...p, ai_ollama_url: e.target.value }))} - disabled={!editMode} - /> +
+ updateConfig(p => ({ ...p, ai_ollama_url: e.target.value }))} + disabled={!editMode} + /> + +
+ {ollamaModels.length > 0 && ( +

{ollamaModels.length} models found

+ )}
)} @@ -1495,38 +1516,28 @@ export function NotificationSettings() {
{config.ai_provider === "ollama" ? ( -
- - -
+ ) : (
{AI_PROVIDERS.find(p => p.value === config.ai_provider)?.model || "default"} diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index f2631177..d91ad481 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -2,9 +2,10 @@ import { useState, useEffect } from "react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card" -import { Wrench, Package, Ruler, HeartPulse, Cpu, MemoryStick, HardDrive, CircleDot, Network, Server, Settings2, FileText, RefreshCw, Shield, AlertTriangle, Info, Loader2, Check } from "lucide-react" +import { Wrench, Package, Ruler, HeartPulse, Cpu, MemoryStick, HardDrive, CircleDot, Network, Server, Settings2, FileText, RefreshCw, Shield, AlertTriangle, Info, Loader2, Check, Database, CloudOff } from "lucide-react" import { NotificationSettings } from "./notification-settings" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" +import { Switch } from "./ui/switch" import { Input } from "./ui/input" import { Badge } from "./ui/badge" import { getNetworkUnit } from "../lib/format-network" @@ -47,6 +48,20 @@ interface ProxMenuxTool { enabled: boolean } +interface RemoteStorage { + name: string + type: string + status: string + total: number + used: number + available: number + percent: number + exclude_health: boolean + exclude_notifications: boolean + excluded_at?: string + reason?: string +} + export function Settings() { const [proxmenuxTools, setProxmenuxTools] = useState([]) const [loadingTools, setLoadingTools] = useState(true) @@ -61,11 +76,17 @@ export function Settings() { const [savedAllHealth, setSavedAllHealth] = useState(false) const [pendingChanges, setPendingChanges] = useState>({}) const [customValues, setCustomValues] = useState>({}) + + // Remote Storage Exclusions + const [remoteStorages, setRemoteStorages] = useState([]) + const [loadingStorages, setLoadingStorages] = useState(true) + const [savingStorage, setSavingStorage] = useState(null) useEffect(() => { loadProxmenuxTools() getUnitsSettings() loadHealthSettings() + loadRemoteStorages() }, []) const loadProxmenuxTools = async () => { @@ -114,6 +135,53 @@ export function Settings() { } } + const loadRemoteStorages = async () => { + try { + const data = await fetchApi("/api/health/remote-storages") + if (data.storages) { + setRemoteStorages(data.storages) + } + } catch (err) { + console.error("Failed to load remote storages:", err) + } finally { + setLoadingStorages(false) + } + } + + const handleStorageExclusionChange = async (storageName: string, storageType: string, excludeHealth: boolean, excludeNotifications: boolean) => { + setSavingStorage(storageName) + try { + // If both are false, remove the exclusion + if (!excludeHealth && !excludeNotifications) { + await fetchApi(`/api/health/storage-exclusions/${encodeURIComponent(storageName)}`, { + method: "DELETE" + }) + } else { + await fetchApi("/api/health/storage-exclusions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + storage_name: storageName, + storage_type: storageType, + exclude_health: excludeHealth, + exclude_notifications: excludeNotifications + }) + }) + } + + // Update local state + setRemoteStorages(prev => prev.map(s => + s.name === storageName + ? { ...s, exclude_health: excludeHealth, exclude_notifications: excludeNotifications } + : s + )) + } catch (err) { + console.error("Failed to update storage exclusion:", err) + } finally { + setSavingStorage(null) + } + } + const getSelectValue = (hours: number, key: string): string => { if (hours === -1) return "-1" const preset = SUPPRESSION_OPTIONS.find(o => o.value === String(hours)) @@ -439,6 +507,120 @@ export function Settings() { + {/* Remote Storage Exclusions */} + + +
+ + Remote Storage Exclusions +
+ + Exclude remote storages (PBS, NFS, CIFS, etc.) from health monitoring and notifications. + Use this for storages that are intentionally offline or have limited API access. + +
+ + {loadingStorages ? ( +
+
+
+ ) : remoteStorages.length === 0 ? ( +
+ +

No remote storages detected

+

+ PBS, NFS, CIFS, and other remote storages will appear here when configured +

+
+ ) : ( +
+ {/* Header */} +
+ Storage + Health + Alerts +
+ + {/* Storage rows */} +
+ {remoteStorages.map((storage) => { + const isExcluded = storage.exclude_health || storage.exclude_notifications + const isSaving = savingStorage === storage.name + const isOffline = storage.status === 'error' || storage.total === 0 + + return ( +
+
+
+
+
+ {storage.name} + + {storage.type} + +
+ {isOffline && ( +

Offline or unavailable

+ )} +
+
+ +
+ {isSaving ? ( + + ) : ( + { + handleStorageExclusionChange( + storage.name, + storage.type, + !checked, + storage.exclude_notifications + ) + }} + /> + )} +
+ +
+ {isSaving ? ( + + ) : ( + { + handleStorageExclusionChange( + storage.name, + storage.type, + storage.exclude_health, + !checked + ) + }} + /> + )} +
+
+ ) + })} +
+ + {/* Info footer */} +
+ +

+ Health: When OFF, the storage won't trigger warnings/critical alerts in the Health Monitor. +
+ Alerts: When OFF, no notifications will be sent for this storage. +

+
+
+ )} + + + {/* Notification Settings */} diff --git a/AppImage/components/storage-overview.tsx b/AppImage/components/storage-overview.tsx index 7ccbccf8..04da43a1 100644 --- a/AppImage/components/storage-overview.tsx +++ b/AppImage/components/storage-overview.tsx @@ -674,11 +674,20 @@ export function StorageOverview() { {proxmoxStorage.storage .filter((storage) => storage && storage.name && storage.used >= 0 && storage.available >= 0) .sort((a, b) => a.name.localeCompare(b.name)) - .map((storage) => ( + .map((storage) => { + // Check if storage is excluded from monitoring + const isExcluded = storage.excluded === true + const hasError = storage.status === "error" && !isExcluded + + return (
@@ -687,27 +696,40 @@ export function StorageOverview() {

{storage.name}

{storage.type} + {isExcluded && ( + + excluded + + )}
{storage.type}

{storage.name}

- {getStatusIcon(storage.status)} + {isExcluded ? ( + + excluded + + ) : ( + getStatusIcon(storage.status) + )}
{/* Desktop: Badge active + Porcentaje */}
- {storage.status} + {isExcluded ? "not monitored" : storage.status} {storage.percent}%
@@ -750,7 +772,8 @@ export function StorageOverview() {
- ))} + ) + })}
diff --git a/AppImage/scripts/flask_health_routes.py b/AppImage/scripts/flask_health_routes.py index a5117d06..829c71b9 100644 --- a/AppImage/scripts/flask_health_routes.py +++ b/AppImage/scripts/flask_health_routes.py @@ -326,3 +326,133 @@ def save_health_settings(): }) except Exception as e: return jsonify({'error': str(e)}), 500 + + +# ── Remote Storage Exclusions Endpoints ── + +@health_bp.route('/api/health/remote-storages', methods=['GET']) +def get_remote_storages(): + """ + Get list of all remote storages with their exclusion status. + Remote storages are those that can be offline (PBS, NFS, CIFS, etc.) + """ + try: + from proxmox_storage_monitor import proxmox_storage_monitor + + # Get current storage status + storage_status = proxmox_storage_monitor.get_storage_status() + all_storages = storage_status.get('available', []) + storage_status.get('unavailable', []) + + # Filter to only remote types + remote_types = health_persistence.REMOTE_STORAGE_TYPES + remote_storages = [s for s in all_storages if s.get('type', '').lower() in remote_types] + + # Get current exclusions + exclusions = {e['storage_name']: e for e in health_persistence.get_excluded_storages()} + + # Combine info + result = [] + for storage in remote_storages: + name = storage.get('name', '') + exclusion = exclusions.get(name, {}) + result.append({ + 'name': name, + 'type': storage.get('type', 'unknown'), + 'status': storage.get('status', 'unknown'), + 'total': storage.get('total', 0), + 'used': storage.get('used', 0), + 'available': storage.get('available', 0), + 'percent': storage.get('percent', 0), + 'exclude_health': exclusion.get('exclude_health', 0) == 1, + 'exclude_notifications': exclusion.get('exclude_notifications', 0) == 1, + 'excluded_at': exclusion.get('excluded_at'), + 'reason': exclusion.get('reason') + }) + + return jsonify({ + 'storages': result, + 'remote_types': list(remote_types) + }) + except ImportError: + return jsonify({'error': 'Storage monitor not available', 'storages': []}), 200 + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@health_bp.route('/api/health/storage-exclusions', methods=['GET']) +def get_storage_exclusions(): + """Get all storage exclusions.""" + try: + exclusions = health_persistence.get_excluded_storages() + return jsonify({'exclusions': exclusions}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@health_bp.route('/api/health/storage-exclusions', methods=['POST']) +def save_storage_exclusion(): + """ + Add or update a storage exclusion. + + Request body: + { + "storage_name": "pbs-backup", + "storage_type": "pbs", + "exclude_health": true, + "exclude_notifications": true, + "reason": "PBS server is offline daily" + } + """ + try: + data = request.get_json() + if not data or 'storage_name' not in data: + return jsonify({'error': 'storage_name is required'}), 400 + + storage_name = data['storage_name'] + storage_type = data.get('storage_type', 'unknown') + exclude_health = data.get('exclude_health', True) + exclude_notifications = data.get('exclude_notifications', True) + reason = data.get('reason') + + # Check if already excluded + existing = health_persistence.get_excluded_storages() + exists = any(e['storage_name'] == storage_name for e in existing) + + if exists: + # Update existing + success = health_persistence.update_storage_exclusion( + storage_name, exclude_health, exclude_notifications + ) + else: + # Add new + success = health_persistence.exclude_storage( + storage_name, storage_type, exclude_health, exclude_notifications, reason + ) + + if success: + return jsonify({ + 'success': True, + 'message': f'Storage {storage_name} exclusion saved', + 'storage_name': storage_name + }) + else: + return jsonify({'error': 'Failed to save exclusion'}), 500 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@health_bp.route('/api/health/storage-exclusions/', methods=['DELETE']) +def delete_storage_exclusion(storage_name): + """Remove a storage from the exclusion list.""" + try: + success = health_persistence.remove_storage_exclusion(storage_name) + if success: + return jsonify({ + 'success': True, + 'message': f'Storage {storage_name} removed from exclusions' + }) + else: + return jsonify({'error': 'Storage not found in exclusions'}), 404 + except Exception as e: + return jsonify({'error': str(e)}), 500 diff --git a/AppImage/scripts/flask_notification_routes.py b/AppImage/scripts/flask_notification_routes.py index f051df8f..4848736a 100644 --- a/AppImage/scripts/flask_notification_routes.py +++ b/AppImage/scripts/flask_notification_routes.py @@ -130,9 +130,10 @@ def get_ollama_models(): with urllib.request.urlopen(req, timeout=10) as resp: result = json.loads(resp.read().decode('utf-8')) - models = [m.get('name', '').split(':')[0] for m in result.get('models', [])] - # Remove duplicates and sort - models = sorted(list(set(models))) + # Keep full model names (including tags like :latest, :3b-instruct-q4_0) + models = [m.get('name', '') for m in result.get('models', []) if m.get('name')] + # Sort alphabetically + models = sorted(models) return jsonify({ 'success': True, 'models': models, diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py index 17714c47..59f8b417 100644 --- a/AppImage/scripts/flask_server.py +++ b/AppImage/scripts/flask_server.py @@ -2639,6 +2639,24 @@ def get_proxmox_storage(): for unavailable_storage in unavailable_storages: if unavailable_storage['name'] not in existing_storage_names: storage_list.append(unavailable_storage) + + # Get storage exclusions to mark excluded storages + try: + excluded_health = health_persistence.get_excluded_storage_names('health') + remote_types = health_persistence.REMOTE_STORAGE_TYPES + + for storage in storage_list: + storage_name = storage.get('name', '') + storage_type = storage.get('type', '').lower() + + # Mark if this is a remote storage type + storage['is_remote'] = storage_type in remote_types + + # Mark if excluded from health monitoring + storage['excluded'] = storage_name in excluded_health + except Exception: + # If exclusion check fails, continue without it + pass return {'storage': storage_list} diff --git a/AppImage/scripts/health_monitor.py b/AppImage/scripts/health_monitor.py index 367b4cde..dba71b46 100644 --- a/AppImage/scripts/health_monitor.py +++ b/AppImage/scripts/health_monitor.py @@ -4431,6 +4431,8 @@ class HealthMonitor: Detects unavailable storages configured in PVE. Returns CRITICAL if any configured storage is unavailable. Returns None if the module is not available. + + Respects storage exclusions: excluded storages are reported as INFO, not CRITICAL. """ if not PROXMOX_STORAGE_AVAILABLE: return None @@ -4443,12 +4445,22 @@ class HealthMonitor: storage_status = proxmox_storage_monitor.get_storage_status() unavailable_storages = storage_status.get('unavailable', []) - if not unavailable_storages: - # All storages are available. We should also clear any previously recorded storage errors. + # Get excluded storage names for health monitoring + excluded_names = health_persistence.get_excluded_storage_names('health') + + # Separate excluded storages from real issues + excluded_unavailable = [s for s in unavailable_storages if s.get('name', '') in excluded_names] + real_unavailable = [s for s in unavailable_storages if s.get('name', '') not in excluded_names] + + if not real_unavailable: + # All non-excluded storages are available. Clear any previously recorded storage errors. active_errors = health_persistence.get_active_errors() for error in active_errors: if error.get('category') == 'storage' and error.get('error_key', '').startswith('storage_unavailable_'): - health_persistence.clear_error(error['error_key']) + # Only clear if not an excluded storage + storage_name = error.get('error_key', '').replace('storage_unavailable_', '') + if storage_name not in excluded_names: + health_persistence.clear_error(error['error_key']) # Build checks from all configured storages for descriptive display available_storages = storage_status.get('available', []) @@ -4460,12 +4472,24 @@ class HealthMonitor: 'status': 'OK', 'detail': f'{st_type} storage available' } + + # Add excluded unavailable storages as INFO (not CRITICAL) + for st in excluded_unavailable: + st_name = st.get('name', 'unknown') + st_type = st.get('type', 'unknown') + checks[st_name] = { + 'status': 'INFO', + 'detail': f'{st_type} storage excluded from monitoring', + 'excluded': True + } + if not checks: checks['proxmox_storages'] = {'status': 'OK', 'detail': 'All storages available'} return {'status': 'OK', 'checks': checks} storage_details = {} - for storage in unavailable_storages: + # Only process non-excluded unavailable storages as errors + for storage in real_unavailable: storage_name = storage['name'] error_key = f'storage_unavailable_{storage_name}' status_detail = storage.get('status_detail', 'unavailable') @@ -4508,6 +4532,17 @@ class HealthMonitor: 'detail': st_info.get('reason', 'Unavailable'), 'dismissable': False } + + # Add excluded unavailable storages as INFO (not as errors) + for st in excluded_unavailable: + st_name = st.get('name', 'unknown') + st_type = st.get('type', 'unknown') + checks[st_name] = { + 'status': 'INFO', + 'detail': f'{st_type} storage excluded from monitoring (offline)', + 'excluded': True + } + # Also add available storages available_list = storage_status.get('available', []) unavail_names = {s['name'] for s in unavailable_storages} @@ -4518,12 +4553,21 @@ class HealthMonitor: 'detail': f'{st.get("type", "unknown")} storage available' } - return { - 'status': 'CRITICAL', - 'reason': f'{len(unavailable_storages)} Proxmox storage(s) unavailable', - 'details': storage_details, - 'checks': checks - } + # Determine overall status based on non-excluded issues only + if real_unavailable: + return { + 'status': 'CRITICAL', + 'reason': f'{len(real_unavailable)} Proxmox storage(s) unavailable', + 'details': storage_details, + 'checks': checks + } + else: + # Only excluded storages are unavailable - this is OK + return { + 'status': 'OK', + 'reason': 'All monitored storages available', + 'checks': checks + } except Exception as e: print(f"[HealthMonitor] Error checking Proxmox storage: {e}") diff --git a/AppImage/scripts/health_persistence.py b/AppImage/scripts/health_persistence.py index 18603964..70a81170 100644 --- a/AppImage/scripts/health_persistence.py +++ b/AppImage/scripts/health_persistence.py @@ -235,6 +235,22 @@ class HealthPersistence: cursor.execute('CREATE INDEX IF NOT EXISTS idx_obs_disk ON disk_observations(disk_registry_id)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_obs_dismissed ON disk_observations(dismissed)') + # ── Remote Storage Exclusions System ── + # Allows users to permanently exclude remote storages (PBS, NFS, CIFS, etc.) + # from health monitoring and notifications + cursor.execute(''' + CREATE TABLE IF NOT EXISTS excluded_storages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + storage_name TEXT UNIQUE NOT NULL, + storage_type TEXT NOT NULL, + excluded_at TEXT NOT NULL, + exclude_health INTEGER DEFAULT 1, + exclude_notifications INTEGER DEFAULT 1, + reason TEXT + ) + ''') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_excluded_storage ON excluded_storages(storage_name)') + conn.commit() conn.close() @@ -1845,5 +1861,172 @@ class HealthPersistence: return 0 + # ── Remote Storage Exclusions Methods ── + + # Types considered "remote" and eligible for exclusion + REMOTE_STORAGE_TYPES = {'pbs', 'nfs', 'cifs', 'glusterfs', 'iscsi', 'iscsidirect', 'cephfs', 'rbd'} + + def is_remote_storage_type(self, storage_type: str) -> bool: + """Check if a storage type is considered remote/external.""" + return storage_type.lower() in self.REMOTE_STORAGE_TYPES + + def get_excluded_storages(self) -> List[Dict[str, Any]]: + """Get list of all excluded remote storages.""" + try: + with self._db_connection(row_factory=True) as conn: + cursor = conn.cursor() + cursor.execute(''' + SELECT storage_name, storage_type, excluded_at, + exclude_health, exclude_notifications, reason + FROM excluded_storages + ''') + return [dict(row) for row in cursor.fetchall()] + except Exception as e: + print(f"[HealthPersistence] Error getting excluded storages: {e}") + return [] + + def is_storage_excluded(self, storage_name: str, check_type: str = 'health') -> bool: + """ + Check if a storage is excluded from monitoring. + + Args: + storage_name: Name of the storage + check_type: 'health' or 'notifications' + + Returns: + True if storage is excluded for the given check type + """ + try: + with self._db_connection() as conn: + cursor = conn.cursor() + column = 'exclude_health' if check_type == 'health' else 'exclude_notifications' + cursor.execute(f''' + SELECT {column} FROM excluded_storages + WHERE storage_name = ? + ''', (storage_name,)) + row = cursor.fetchone() + return row is not None and row[0] == 1 + except Exception: + return False + + def exclude_storage(self, storage_name: str, storage_type: str, + exclude_health: bool = True, exclude_notifications: bool = True, + reason: str = None) -> bool: + """ + Add a storage to the exclusion list. + + Args: + storage_name: Name of the storage to exclude + storage_type: Type of storage (pbs, nfs, etc.) + exclude_health: Whether to exclude from health monitoring + exclude_notifications: Whether to exclude from notifications + reason: Optional reason for exclusion + + Returns: + True if successfully excluded + """ + try: + now = datetime.now().isoformat() + with self._db_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO excluded_storages + (storage_name, storage_type, excluded_at, exclude_health, exclude_notifications, reason) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(storage_name) DO UPDATE SET + exclude_health = excluded.exclude_health, + exclude_notifications = excluded.exclude_notifications, + reason = excluded.reason + ''', (storage_name, storage_type, now, + 1 if exclude_health else 0, + 1 if exclude_notifications else 0, + reason)) + conn.commit() + return True + except Exception as e: + print(f"[HealthPersistence] Error excluding storage: {e}") + return False + + def update_storage_exclusion(self, storage_name: str, + exclude_health: Optional[bool] = None, + exclude_notifications: Optional[bool] = None) -> bool: + """ + Update exclusion settings for a storage. + + Args: + storage_name: Name of the storage + exclude_health: New value for health exclusion (None = don't change) + exclude_notifications: New value for notifications exclusion (None = don't change) + + Returns: + True if successfully updated + """ + try: + with self._db_connection() as conn: + cursor = conn.cursor() + + updates = [] + values = [] + + if exclude_health is not None: + updates.append('exclude_health = ?') + values.append(1 if exclude_health else 0) + + if exclude_notifications is not None: + updates.append('exclude_notifications = ?') + values.append(1 if exclude_notifications else 0) + + if not updates: + return True + + values.append(storage_name) + cursor.execute(f''' + UPDATE excluded_storages + SET {', '.join(updates)} + WHERE storage_name = ? + ''', values) + conn.commit() + return cursor.rowcount > 0 + except Exception as e: + print(f"[HealthPersistence] Error updating storage exclusion: {e}") + return False + + def remove_storage_exclusion(self, storage_name: str) -> bool: + """Remove a storage from the exclusion list.""" + try: + with self._db_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + DELETE FROM excluded_storages WHERE storage_name = ? + ''', (storage_name,)) + conn.commit() + return cursor.rowcount > 0 + except Exception as e: + print(f"[HealthPersistence] Error removing storage exclusion: {e}") + return False + + def get_excluded_storage_names(self, check_type: str = 'health') -> set: + """ + Get set of storage names excluded for a specific check type. + + Args: + check_type: 'health' or 'notifications' + + Returns: + Set of excluded storage names + """ + try: + with self._db_connection() as conn: + cursor = conn.cursor() + column = 'exclude_health' if check_type == 'health' else 'exclude_notifications' + cursor.execute(f''' + SELECT storage_name FROM excluded_storages + WHERE {column} = 1 + ''') + return {row[0] for row in cursor.fetchall()} + except Exception: + return set() + + # Global instance health_persistence = HealthPersistence() diff --git a/AppImage/scripts/notification_manager.py b/AppImage/scripts/notification_manager.py index 3f1f6815..691c2d8b 100644 --- a/AppImage/scripts/notification_manager.py +++ b/AppImage/scripts/notification_manager.py @@ -648,6 +648,19 @@ class NotificationManager: if self._is_backup_running(): return + # Check storage exclusions for storage-related events. + # If the storage is excluded from notifications, suppress the event entirely. + _STORAGE_EVENTS = {'storage_unavailable', 'storage_low_space', 'storage_warning', 'storage_error'} + if event.event_type in _STORAGE_EVENTS: + storage_name = event.data.get('storage_name') or event.data.get('name') + if storage_name: + try: + from health_persistence import health_persistence + if health_persistence.is_storage_excluded(storage_name, 'notifications'): + return # Storage is excluded from notifications, skip silently + except Exception: + pass # Continue if check fails + # Check cooldown if not self._check_cooldown(event): return