Agregar interfaz de notificaciones

main
EMOTIONS-HUNTER 4 weeks ago
parent 7d8959f164
commit 36743ed86b

@ -91,7 +91,10 @@ function validateNotificationConfig(rawConfig) {
} }
async function writeNotificationConfig(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.mkdir(path.dirname(NOTIFICATION_CONFIG_FILE), { recursive: true });
await fs.writeFile( await fs.writeFile(
@ -102,6 +105,26 @@ async function writeNotificationConfig(rawConfig) {
return config; 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) { function redactNotificationConfig(config) {
return { return {
...config, ...config,
@ -239,6 +262,7 @@ module.exports = {
dispatchAlarmNotifications, dispatchAlarmNotifications,
readNotificationConfig, readNotificationConfig,
redactNotificationConfig, redactNotificationConfig,
resolvePreservedSecrets,
shouldNotify, shouldNotify,
validateNotificationConfig, validateNotificationConfig,
writeNotificationConfig writeNotificationConfig

@ -546,6 +546,10 @@ h1 {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.notification-config-group {
grid-template-columns: 1fr;
}
.metric-card { .metric-card {
min-height: 178px; min-height: 178px;
} }
@ -643,6 +647,38 @@ h1 {
gap: 6px; 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, .terminal-card,
.calibration-card { .calibration-card {
overflow: hidden; overflow: hidden;

@ -9,6 +9,8 @@ let currentHistoryRetentionDays = 30;
let historyIntervalId = null; let historyIntervalId = null;
let dashboardUpdateInProgress = false; let dashboardUpdateInProgress = false;
let alarmControlsInitialized = false; let alarmControlsInitialized = false;
let notificationConfigLoaded = false;
let telegramTokenConfigured = false;
// Diccionario para traducir estados en la UI sin romper las clases CSS // Diccionario para traducir estados en la UI sin romper las clases CSS
const stateTranslations = { 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() { async function readAlarmConfig() {
try { try {
const response = await fetch('/api/config/alarms', { const response = await fetch('/api/config/alarms', {
@ -854,6 +922,7 @@ updateDashboard();
setInterval(updateDashboard, POLLING_INTERVAL_MS); setInterval(updateDashboard, POLLING_INTERVAL_MS);
updateHistoricalTrends(); updateHistoricalTrends();
startHistoryPolling(); startHistoryPolling();
loadNotificationConfig();
// --- FUNCIONES DE CONTROL DE ALMACENAMIENTO --- // --- FUNCIONES DE CONTROL DE ALMACENAMIENTO ---

@ -206,6 +206,27 @@
<button class="btn-command" type="button" onclick="saveApiToken()">Guardar</button> <button class="btn-command" type="button" onclick="saveApiToken()">Guardar</button>
</div> </div>
<fieldset class="notification-config-group">
<legend>Notificaciones</legend>
<label><input id="notifications-enabled" type="checkbox"> Activas</label>
<label>
Severidad
<select id="notification-min-severity">
<option value="WARNING">WARNING</option>
<option value="OFFLINE">OFFLINE</option>
<option value="CRITICAL">CRITICAL</option>
</select>
</label>
<label><input id="webhook-enabled" type="checkbox"> Webhook</label>
<input id="webhook-url" type="url" placeholder="https://example.com/webhook">
<label><input id="telegram-enabled" type="checkbox"> Telegram</label>
<input id="telegram-chat-id" type="text" placeholder="Chat ID">
<input id="telegram-bot-token" type="password" autocomplete="off" placeholder="Bot token">
<button class="btn-command" type="button" onclick="saveNotificationConfig()">
Guardar notificaciones
</button>
</fieldset>
<div style="margin-top: auto;"> <div style="margin-top: auto;">
<button class="btn-command" onclick="clearHistoricalData()" <button class="btn-command" onclick="clearHistoricalData()"
style="padding: 8px 15px; background-color: #ff4444; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;"> style="padding: 8px 15px; background-color: #ff4444; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">

@ -44,6 +44,7 @@ const {
} = require('../api/acquisition-service'); } = require('../api/acquisition-service');
const { const {
dispatchAlarmNotifications, dispatchAlarmNotifications,
readNotificationConfig,
writeNotificationConfig writeNotificationConfig
} = require('../api/notification-service'); } = require('../api/notification-service');
@ -456,6 +457,38 @@ test('dispatches webhook notifications for eligible alarm events', async () => {
} }
}); });
test('preserves existing Telegram token when requested by notification UI', async () => {
await writeNotificationConfig({
enabled: true,
minSeverity: 'CRITICAL',
channels: {
webhook: { enabled: false, url: '', headers: {} },
telegram: {
enabled: true,
botToken: '123456:secret',
chatId: '42'
}
}
});
await writeNotificationConfig({
enabled: true,
minSeverity: 'WARNING',
channels: {
webhook: { enabled: false, url: '', headers: {} },
telegram: {
enabled: true,
botToken: '__KEEP__',
chatId: '43'
}
}
});
const config = await readNotificationConfig();
assert.equal(config.channels.telegram.botToken, '123456:secret');
assert.equal(config.channels.telegram.chatId, '43');
});
test('demo acquisition writes live JSON and CSV files', async () => { test('demo acquisition writes live JSON and CSV files', async () => {
await fetch(`${baseUrl}/api/config/runtime`, { await fetch(`${baseUrl}/api/config/runtime`, {
method: 'POST', method: 'POST',

@ -17,6 +17,9 @@ test('dashboard starts live polling and exposes required controls', () => {
assert.match(html, /name="enabled-sensor"/); assert.match(html, /name="enabled-sensor"/);
assert.match(html, /id="api-token-input"/); assert.match(html, /id="api-token-input"/);
assert.match(html, /id="history-retention-days"/); assert.match(html, /id="history-retention-days"/);
assert.match(html, /id="notifications-enabled"/);
assert.match(html, /id="webhook-url"/);
assert.match(html, /id="telegram-bot-token"/);
assert.match(html, /id="alarm-history-list"/); assert.match(html, /id="alarm-history-list"/);
assert.match(html, /id="alarm-threshold-controls"/); assert.match(html, /id="alarm-threshold-controls"/);
assert.match(dashboard, /updateDashboard\(\);/); assert.match(dashboard, /updateDashboard\(\);/);
@ -26,6 +29,8 @@ test('dashboard starts live polling and exposes required controls', () => {
assert.match(dashboard, /saveAlarmThresholds/); assert.match(dashboard, /saveAlarmThresholds/);
assert.match(dashboard, /\/api\/config\/alarms/); assert.match(dashboard, /\/api\/config\/alarms/);
assert.match(dashboard, /updateHistoryRetention/); assert.match(dashboard, /updateHistoryRetention/);
assert.match(dashboard, /saveNotificationConfig/);
assert.match(dashboard, /\/api\/config\/notifications/);
assert.match(dashboard, /getApiHeaders/); assert.match(dashboard, /getApiHeaders/);
assert.match( assert.match(
dashboard, dashboard,

Loading…
Cancel
Save