diff --git a/api/notification-service.js b/api/notification-service.js index c922c70..6f10704 100644 --- a/api/notification-service.js +++ b/api/notification-service.js @@ -91,7 +91,10 @@ function validateNotificationConfig(rawConfig) { } async function writeNotificationConfig(rawConfig) { - const config = validateNotificationConfig(rawConfig); + const currentConfig = await readNotificationConfig(); + const config = validateNotificationConfig( + resolvePreservedSecrets(rawConfig, currentConfig) + ); await fs.mkdir(path.dirname(NOTIFICATION_CONFIG_FILE), { recursive: true }); await fs.writeFile( @@ -102,6 +105,26 @@ async function writeNotificationConfig(rawConfig) { return config; } +function resolvePreservedSecrets(rawConfig, currentConfig) { + const nextConfig = mergeNotificationConfig(rawConfig); + + if ( + nextConfig.channels.telegram.botToken === '__KEEP__' && + currentConfig.channels.telegram.botToken + ) { + nextConfig.channels.telegram.botToken = currentConfig.channels.telegram.botToken; + } + + if ( + nextConfig.channels.webhook.headers === '__KEEP__' && + currentConfig.channels.webhook.headers + ) { + nextConfig.channels.webhook.headers = currentConfig.channels.webhook.headers; + } + + return nextConfig; +} + function redactNotificationConfig(config) { return { ...config, @@ -239,6 +262,7 @@ module.exports = { dispatchAlarmNotifications, readNotificationConfig, redactNotificationConfig, + resolvePreservedSecrets, shouldNotify, validateNotificationConfig, writeNotificationConfig diff --git a/frontend/dashboard.css b/frontend/dashboard.css index 6390fbf..7b604da 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -546,6 +546,10 @@ h1 { grid-template-columns: 1fr; } + .notification-config-group { + grid-template-columns: 1fr; + } + .metric-card { min-height: 178px; } @@ -643,6 +647,38 @@ h1 { gap: 6px; } +.notification-config-group { + display: grid; + grid-template-columns: repeat(4, minmax(0, auto)); + align-items: end; + gap: 10px; + margin: 0; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: 8px; +} + +.notification-config-group legend { + color: var(--muted); + font-size: 0.9rem; + font-weight: 700; +} + +.notification-config-group label { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--muted) !important; +} + +.notification-config-group input, +.notification-config-group select { + min-height: 34px; + padding: 6px 8px; + border: 1px solid var(--border); + border-radius: 6px; +} + .terminal-card, .calibration-card { overflow: hidden; diff --git a/frontend/dashboard.js b/frontend/dashboard.js index eba0800..20f9bd4 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -9,6 +9,8 @@ let currentHistoryRetentionDays = 30; let historyIntervalId = null; let dashboardUpdateInProgress = false; let alarmControlsInitialized = false; +let notificationConfigLoaded = false; +let telegramTokenConfigured = false; // Diccionario para traducir estados en la UI sin romper las clases CSS const stateTranslations = { @@ -319,6 +321,72 @@ async function readRuntimeConfig() { } } +async function loadNotificationConfig() { + try { + const response = await fetch('/api/config/notifications', { cache: 'no-store' }); + const result = await response.json(); + + if (!response.ok || !result.config) return; + + const config = result.config; + telegramTokenConfigured = config.channels.telegram.botToken === "[configured]"; + document.getElementById("notifications-enabled").checked = config.enabled === true; + document.getElementById("notification-min-severity").value = config.minSeverity || "CRITICAL"; + document.getElementById("webhook-enabled").checked = config.channels.webhook.enabled === true; + document.getElementById("webhook-url").value = config.channels.webhook.url || ""; + document.getElementById("telegram-enabled").checked = config.channels.telegram.enabled === true; + document.getElementById("telegram-chat-id").value = config.channels.telegram.chatId || ""; + document.getElementById("telegram-bot-token").placeholder = telegramTokenConfigured + ? "Token configurado" + : "Bot token"; + notificationConfigLoaded = true; + } catch (error) { + console.warn("No se pudo cargar la configuracion de notificaciones:", error); + } +} + +window.saveNotificationConfig = async function () { + const botTokenInput = document.getElementById("telegram-bot-token"); + const botToken = botTokenInput.value.trim(); + const config = { + enabled: document.getElementById("notifications-enabled").checked, + minSeverity: document.getElementById("notification-min-severity").value, + channels: { + webhook: { + enabled: document.getElementById("webhook-enabled").checked, + url: document.getElementById("webhook-url").value.trim(), + headers: {} + }, + telegram: { + enabled: document.getElementById("telegram-enabled").checked, + botToken: botToken || (telegramTokenConfigured ? "__KEEP__" : ""), + chatId: document.getElementById("telegram-chat-id").value.trim() + } + } + }; + + try { + const response = await fetch('/api/config/notifications', { + method: 'POST', + headers: getApiHeaders({ 'Content-Type': 'application/json' }), + body: JSON.stringify({ config }) + }); + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.error || "No se pudo guardar notificaciones."); + } + + botTokenInput.value = ""; + notificationConfigLoaded = false; + await loadNotificationConfig(); + alert("Configuracion de notificaciones actualizada."); + } catch (error) { + console.error("Error guardando notificaciones:", error); + alert(error.message); + } +}; + async function readAlarmConfig() { try { const response = await fetch('/api/config/alarms', { @@ -854,6 +922,7 @@ updateDashboard(); setInterval(updateDashboard, POLLING_INTERVAL_MS); updateHistoricalTrends(); startHistoryPolling(); +loadNotificationConfig(); // --- FUNCIONES DE CONTROL DE ALMACENAMIENTO --- diff --git a/frontend/index.html b/frontend/index.html index bbc2123..b6f06ee 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -206,6 +206,27 @@ +
+