From d5954a3a326324ffe6a3189e08128ab4d6ef8024 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Thu, 19 Feb 2026 20:51:54 +0100 Subject: [PATCH] Update notification service --- AppImage/components/notification-settings.tsx | 43 +++++++++++++++++- AppImage/scripts/notification_channels.py | 6 ++- AppImage/scripts/notification_manager.py | 45 ++++++++++++++++--- 3 files changed, 84 insertions(+), 10 deletions(-) diff --git a/AppImage/components/notification-settings.tsx b/AppImage/components/notification-settings.tsx index 8fa1216f..c6ecf490 100644 --- a/AppImage/components/notification-settings.tsx +++ b/AppImage/components/notification-settings.tsx @@ -202,6 +202,35 @@ export function NotificationSettings() { })) } + /** Flatten the nested NotificationConfig into the flat key-value map the backend expects. */ + const flattenConfig = (cfg: NotificationConfig): Record => { + const flat: Record = { + enabled: String(cfg.enabled), + severity_filter: cfg.severity_filter, + ai_enabled: String(cfg.ai_enabled), + ai_provider: cfg.ai_provider, + ai_api_key: cfg.ai_api_key, + ai_model: cfg.ai_model, + hostname: cfg.hostname, + webhook_secret: cfg.webhook_secret, + webhook_allowed_ips: cfg.webhook_allowed_ips, + pbs_host: cfg.pbs_host, + pve_host: cfg.pve_host, + pbs_trusted_sources: cfg.pbs_trusted_sources, + } + // Flatten channels: { telegram: { enabled, bot_token, chat_id } } -> telegram.enabled, telegram.bot_token, ... + for (const [chName, chCfg] of Object.entries(cfg.channels)) { + for (const [field, value] of Object.entries(chCfg)) { + flat[`${chName}.${field}`] = String(value ?? "") + } + } + // Flatten event_categories: { system: true, backups: false } -> events.system, events.backups + for (const [cat, enabled] of Object.entries(cfg.event_categories)) { + flat[`events.${cat}`] = String(enabled) + } + return flat + } + const handleSave = async () => { setSaving(true) try { @@ -217,9 +246,10 @@ export function NotificationSettings() { } } + const payload = flattenConfig(config) await fetchApi("/api/notifications/settings", { method: "POST", - body: JSON.stringify(config), + body: JSON.stringify(payload), }) setOriginalConfig(config) setHasChanges(false) @@ -244,6 +274,15 @@ export function NotificationSettings() { setTesting(channel) setTestResult(null) try { + // Auto-save current config before testing so backend has latest channel data + const payload = flattenConfig(config) + await fetchApi("/api/notifications/settings", { + method: "POST", + body: JSON.stringify(payload), + }) + setOriginalConfig(config) + setHasChanges(false) + const data = await fetchApi<{ success: boolean message?: string @@ -642,7 +681,7 @@ matcher: proxmenux-pbs updateChannel("telegram", "bot_token", e.target.value)} /> diff --git a/AppImage/scripts/notification_channels.py b/AppImage/scripts/notification_channels.py index 79393e50..9cb6255f 100644 --- a/AppImage/scripts/notification_channels.py +++ b/AppImage/scripts/notification_channels.py @@ -133,7 +133,11 @@ class TelegramChannel(NotificationChannel): def __init__(self, bot_token: str, chat_id: str): super().__init__() - self.bot_token = bot_token.strip() + token = bot_token.strip() + # Strip 'bot' prefix if user included it (API_BASE already adds it) + if token.lower().startswith('bot') and ':' in token[3:]: + token = token[3:] + self.bot_token = token self.chat_id = chat_id.strip() def validate_config(self) -> Tuple[bool, str]: diff --git a/AppImage/scripts/notification_manager.py b/AppImage/scripts/notification_manager.py index e8b5335c..9920bf7a 100644 --- a/AppImage/scripts/notification_manager.py +++ b/AppImage/scripts/notification_manager.py @@ -937,19 +937,50 @@ class NotificationManager: return {'success': False, 'error': str(e)} def get_settings(self) -> Dict[str, Any]: - """Get all notification settings for the UI.""" + """Get all notification settings for the UI. + + Returns a structure matching the frontend's NotificationConfig shape + so the round-trip (GET -> edit -> POST) is seamless. + """ if not self._config: self._load_config() - return { + # Build nested channels object matching frontend ChannelConfig + channels = {} + for ch_type, info in CHANNEL_TYPES.items(): + ch_cfg: Dict[str, Any] = { + 'enabled': self._config.get(f'{ch_type}.enabled', 'false') == 'true', + } + for config_key in info['config_keys']: + ch_cfg[config_key] = self._config.get(f'{ch_type}.{config_key}', '') + channels[ch_type] = ch_cfg + + # Build event_categories dict + # EVENT_GROUPS is a dict: { 'system': {...}, 'vm_ct': {...}, ... } + event_categories = {} + for group_key in EVENT_GROUPS: + event_categories[group_key] = self._config.get(f'events.{group_key}', 'true') == 'true' + + config = { 'enabled': self._enabled, - 'settings': {f'{SETTINGS_PREFIX}{k}': v for k, v in self._config.items()}, - 'channels': self.list_channels()['channels'], - 'event_groups': EVENT_GROUPS, - 'event_types': get_event_types_by_group(), - 'default_events': get_default_enabled_events(), + 'channels': channels, + 'severity_filter': self._config.get('severity_filter', 'warning'), + 'event_categories': event_categories, + 'ai_enabled': self._config.get('ai_enabled', 'false') == 'true', + 'ai_provider': self._config.get('ai_provider', 'openai'), + 'ai_api_key': self._config.get('ai_api_key', ''), + 'ai_model': self._config.get('ai_model', ''), + 'hostname': self._config.get('hostname', ''), 'webhook_secret': self._config.get('webhook_secret', ''), 'webhook_allowed_ips': self._config.get('webhook_allowed_ips', ''), + 'pbs_host': self._config.get('pbs_host', ''), + 'pve_host': self._config.get('pve_host', ''), + 'pbs_trusted_sources': self._config.get('pbs_trusted_sources', ''), + } + + return { + 'success': True, + 'config': config, } def save_settings(self, settings: Dict[str, str]) -> Dict[str, Any]: