Files
dashcaddy/status/js/notification-settings.js
T

467 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ========== NOTIFICATION SETTINGS ==========
(function() {
const errorHandler = new ErrorHandler();
// Inject modal HTML
injectModal('notifications-modal', `<div id="notifications-modal" class="weather-modal">
<div class="weather-modal-content" style="min-width: 550px; max-width: 650px;">
<h3>🔔 Notification Settings</h3>
<!-- Master Toggle -->
<div class="accent-info-box">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="notifications-enabled" />
<div>
<span style="font-weight: 600; color: var(--accent);">Enable Notifications</span>
<div class="text-tiny-muted">Receive alerts when containers go up/down</div>
</div>
</label>
</div>
<!-- Providers Section -->
<h4 class="section-heading">Notification Providers</h4>
<!-- Discord -->
<div class="notification-provider provider-card">
<div class="provider-header">
<label class="checkbox-label">
<input type="checkbox" id="discord-enabled" />
<span class="fw-500">Discord</span>
</label>
<button id="discord-test" class="test-btn btn-xs">Test</button>
</div>
<div id="discord-config" style="display: none;">
<label class="field-label-sm">Webhook URL:</label>
<input type="text" id="discord-webhook" placeholder="https://discord.com/api/v1/webhooks/..." style="width: 100%;" />
</div>
</div>
<!-- Telegram -->
<div class="notification-provider provider-card">
<div class="provider-header">
<label class="checkbox-label">
<input type="checkbox" id="telegram-enabled" />
<span class="fw-500">Telegram</span>
</label>
<button id="telegram-test" class="test-btn btn-xs">Test</button>
</div>
<div id="telegram-config" style="display: none;">
<div class="grid-2col">
<div>
<label class="field-label-sm">Bot Token:</label>
<input type="text" id="telegram-bot-token" placeholder="123456:ABC..." />
</div>
<div>
<label class="field-label-sm">Chat ID:</label>
<input type="text" id="telegram-chat-id" placeholder="-1001234567890" />
</div>
</div>
</div>
</div>
<!-- ntfy.sh -->
<div class="notification-provider provider-card">
<div class="provider-header">
<label class="checkbox-label">
<input type="checkbox" id="ntfy-enabled" />
<span class="fw-500">ntfy.sh</span>
</label>
<button id="ntfy-test" class="test-btn btn-xs">Test</button>
</div>
<div id="ntfy-config" style="display: none;">
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 8px;">
<div>
<label class="field-label-sm">Server URL:</label>
<input type="text" id="ntfy-server" placeholder="https://ntfy.sh" value="https://ntfy.sh" />
</div>
<div>
<label class="field-label-sm">Topic:</label>
<input type="text" id="ntfy-topic" placeholder="dashcaddy-alerts" />
</div>
</div>
</div>
</div>
<!-- Email -->
<div class="notification-provider provider-card">
<div class="provider-header">
<label class="checkbox-label">
<input type="checkbox" id="email-enabled" />
<span class="fw-500">Email (SMTP)</span>
</label>
<button id="email-test" class="test-btn btn-xs">Test</button>
</div>
<div id="email-config" style="display: none;">
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 8px; margin-bottom: 8px;">
<div>
<label class="field-label-sm">SMTP Host:</label>
<input type="text" id="email-host" placeholder="smtp.gmail.com" />
</div>
<div>
<label class="field-label-sm">Port:</label>
<input type="number" id="email-port" value="587" placeholder="587" />
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px;">
<div>
<label class="field-label-sm">Username:</label>
<input type="text" id="email-user" placeholder="user@gmail.com" />
</div>
<div>
<label class="field-label-sm">Password:</label>
<input type="password" id="email-pass" placeholder="app password" />
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<div>
<label class="field-label-sm">From:</label>
<input type="text" id="email-from" placeholder="DashCaddy &lt;noreply@example.com&gt;" />
</div>
<div>
<label class="field-label-sm">To:</label>
<input type="text" id="email-to" placeholder="admin@example.com" />
</div>
</div>
<label style="display: flex; align-items: center; gap: 6px; margin-top: 8px; font-size: 0.8rem;">
<input type="checkbox" id="email-secure" /> Use TLS (port 465)
</label>
</div>
</div>
<!-- Health Check Settings -->
<h4 class="section-heading">Health Monitoring</h4>
<div style="padding: 12px; background: var(--card-base); border-radius: 8px; border: 1px solid var(--border);">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; margin-bottom: 10px;">
<input type="checkbox" id="health-check-enabled" />
<div>
<span class="fw-500">Enable Health Monitoring</span>
<div class="text-tiny-muted">Periodically check container status</div>
</div>
</label>
<div id="health-check-config" style="display: flex; align-items: center; gap: 10px;">
<label class="field-label-sm">Check interval:</label>
<select id="health-check-interval" style="width: auto;">
<option value="1">1 minute</option>
<option value="5" selected>5 minutes</option>
<option value="15">15 minutes</option>
<option value="30">30 minutes</option>
<option value="60">1 hour</option>
</select>
<button id="health-check-now" style="padding: 4px 10px; font-size: 0.75rem; margin-left: auto;">Check Now</button>
</div>
<div id="health-check-status" style="font-size: 0.75rem; color: var(--muted); margin-top: 8px;">
Last check: Never
</div>
</div>
<!-- Event Types -->
<h4 class="section-heading">Events to Notify</h4>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px; padding: 12px; background: var(--card-base); border-radius: 8px; border: 1px solid var(--border);">
<label class="checkbox-label-sm">
<input type="checkbox" id="event-container-down" checked /> Container Down
</label>
<label class="checkbox-label-sm">
<input type="checkbox" id="event-container-up" checked /> Container Recovered
</label>
<label class="checkbox-label-sm">
<input type="checkbox" id="event-deploy-success" checked /> Deployment Success
</label>
<label class="checkbox-label-sm">
<input type="checkbox" id="event-deploy-failed" checked /> Deployment Failed
</label>
<label class="checkbox-label-sm" style="grid-column: 1 / -1;">
<input type="checkbox" id="event-resource-alert" checked /> Resource Alerts
</label>
</div>
<!-- History -->
<h4 class="section-heading">Notification History</h4>
<div id="notification-history" style="max-height: 150px; overflow-y: auto; padding: 8px; background: var(--card-base); border-radius: 8px; border: 1px solid var(--border); font-size: 0.8rem;">
<div style="color: var(--muted); text-align: center; padding: 20px;">No notifications yet</div>
</div>
<div id="last-notification-sent" style="font-size: 0.75rem; color: var(--muted); text-align: center; margin-top: 6px;"></div>
<!-- Buttons -->
<div class="weather-modal-buttons modal-footer-bar">
<button id="notifications-send-test" class="btn-secondary" style="margin-right: auto;">Send Test</button>
<button id="notifications-cancel">Cancel</button>
<button id="notifications-save" class="btn-accent">Save Settings</button>
</div>
</div>
</div>`);
const modal = document.getElementById('notifications-modal');
const openBtn = document.getElementById('manage-notifications');
const saveBtn = document.getElementById('notifications-save');
const cancelBtn = document.getElementById('notifications-cancel');
// Provider toggle handlers
['discord', 'telegram', 'ntfy', 'email'].forEach(provider => {
const checkbox = document.getElementById(`${provider}-enabled`);
const config = document.getElementById(`${provider}-config`);
checkbox?.addEventListener('change', () => {
config.style.display = checkbox.checked ? 'block' : 'none';
});
});
// Health check toggle
const healthCheckEnabled = document.getElementById('health-check-enabled');
const healthCheckConfig = document.getElementById('health-check-config');
healthCheckEnabled?.addEventListener('change', () => {
healthCheckConfig.style.opacity = healthCheckEnabled.checked ? '1' : '0.5';
});
// Load notification config from API
async function loadNotificationConfig() {
try {
const response = await fetch('/api/v1/notifications/config');
const data = await response.json();
if (data.success) {
const config = data.config;
// Master toggle
document.getElementById('notifications-enabled').checked = config.enabled;
// Providers
document.getElementById('discord-enabled').checked = config.providers?.discord?.enabled || false;
document.getElementById('telegram-enabled').checked = config.providers?.telegram?.enabled || false;
document.getElementById('ntfy-enabled').checked = config.providers?.ntfy?.enabled || false;
document.getElementById('email-enabled').checked = config.providers?.email?.enabled || false;
// Show/hide config sections
document.getElementById('discord-config').style.display = config.providers?.discord?.enabled ? 'block' : 'none';
document.getElementById('telegram-config').style.display = config.providers?.telegram?.enabled ? 'block' : 'none';
document.getElementById('ntfy-config').style.display = config.providers?.ntfy?.enabled ? 'block' : 'none';
document.getElementById('email-config').style.display = config.providers?.email?.enabled ? 'block' : 'none';
// ntfy server URL
if (config.providers?.ntfy?.serverUrl) {
document.getElementById('ntfy-server').value = config.providers.ntfy.serverUrl;
}
// email fields
if (config.providers?.email?.host) document.getElementById('email-host').value = config.providers.email.host;
if (config.providers?.email?.from) document.getElementById('email-from').value = config.providers.email.from;
// Health check
document.getElementById('health-check-enabled').checked = config.healthCheck?.enabled || false;
if (config.healthCheck?.intervalMinutes) {
document.getElementById('health-check-interval').value = config.healthCheck.intervalMinutes;
}
if (config.healthCheck?.lastCheck) {
document.getElementById('health-check-status').textContent =
`Last check: ${new Date(config.healthCheck.lastCheck).toLocaleString()}`;
}
// Events
document.getElementById('event-container-down').checked = config.events?.containerDown !== false;
document.getElementById('event-container-up').checked = config.events?.containerUp !== false;
document.getElementById('event-deploy-success').checked = config.events?.deploymentSuccess !== false;
document.getElementById('event-deploy-failed').checked = config.events?.deploymentFailed !== false;
document.getElementById('event-resource-alert').checked = config.events?.resourceAlert !== false;
}
} catch (error) {
errorHandler.logError('[Notifications] Load Config', error, { function: 'loadConfig' });
}
}
// Load notification history
async function loadNotificationHistory() {
try {
const response = await fetch('/api/v1/notifications/history?limit=10');
const data = await response.json();
const container = document.getElementById('notification-history');
if (data.success && data.history?.length > 0) {
container.innerHTML = data.history.map(item => {
const date = new Date(item.timestamp).toLocaleString();
const typeColors = {
success: 'var(--ok-fg)',
error: 'var(--bad-fg)',
warning: '#f39c12',
info: 'var(--accent)'
};
return `
<div style="padding: 6px 8px; border-bottom: 1px solid var(--border); display: flex; gap: 8px; align-items: flex-start;">
<span style="color: ${typeColors[item.type] || 'var(--muted)'}">${item.type === 'success' ? '✓' : item.type === 'error' ? '✗' : ''}</span>
<div style="flex: 1; min-width: 0;">
<div style="font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(item.title)}</div>
<div style="font-size: 0.7rem; color: var(--muted);">${date}</div>
</div>
</div>
`;
}).join('');
} else {
container.innerHTML = '<div style="color: var(--muted); text-align: center; padding: 20px;">No notifications yet</div>';
}
} catch (error) {
errorHandler.logError('[Notifications] Load History', error, { function: 'loadHistory' });
}
}
// Save notification config
async function saveNotificationConfig() {
try {
const config = {
enabled: document.getElementById('notifications-enabled').checked,
providers: {
discord: {
enabled: document.getElementById('discord-enabled').checked,
webhookUrl: document.getElementById('discord-webhook').value.trim()
},
telegram: {
enabled: document.getElementById('telegram-enabled').checked,
botToken: document.getElementById('telegram-bot-token').value.trim(),
chatId: document.getElementById('telegram-chat-id').value.trim()
},
ntfy: {
enabled: document.getElementById('ntfy-enabled').checked,
serverUrl: document.getElementById('ntfy-server').value.trim() || 'https://ntfy.sh',
topic: document.getElementById('ntfy-topic').value.trim()
},
email: {
enabled: document.getElementById('email-enabled').checked,
host: document.getElementById('email-host').value.trim(),
port: parseInt(document.getElementById('email-port').value) || 587,
secure: document.getElementById('email-secure').checked,
user: document.getElementById('email-user').value.trim(),
pass: document.getElementById('email-pass').value.trim(),
from: document.getElementById('email-from').value.trim(),
to: document.getElementById('email-to').value.trim()
}
},
events: {
containerDown: document.getElementById('event-container-down').checked,
containerUp: document.getElementById('event-container-up').checked,
deploymentSuccess: document.getElementById('event-deploy-success').checked,
deploymentFailed: document.getElementById('event-deploy-failed').checked,
resourceAlert: document.getElementById('event-resource-alert').checked
},
healthCheck: {
enabled: document.getElementById('health-check-enabled').checked,
intervalMinutes: parseInt(document.getElementById('health-check-interval').value) || 5
}
};
const response = await secureFetch('/api/v1/notifications/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
const data = await response.json();
if (data.success) {
showNotification('Notification settings saved', 'success', 3000);
modal.classList.remove('show');
} else {
showNotification(`Failed to save: ${data.error}`, 'error', 3000);
}
} catch (error) {
showNotification(`Error: ${error.message}`, 'error', 3000);
}
}
// Test notification handlers
async function testProvider(provider) {
try {
const response = await secureFetch('/api/v1/notifications/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider })
});
const data = await response.json();
if (data.success) {
showNotification(`Test ${provider} notification sent!`, 'success', 3000);
} else {
showNotification(`Test failed: ${data.error}`, 'error', 3000);
}
} catch (error) {
showNotification(`Error: ${error.message}`, 'error', 3000);
}
}
document.getElementById('discord-test')?.addEventListener('click', () => testProvider('discord'));
document.getElementById('telegram-test')?.addEventListener('click', () => testProvider('telegram'));
document.getElementById('ntfy-test')?.addEventListener('click', () => testProvider('ntfy'));
document.getElementById('email-test')?.addEventListener('click', () => testProvider('email'));
// Health check now button
document.getElementById('health-check-now')?.addEventListener('click', async () => {
try {
const response = await secureFetch('/api/v1/notifications/health-check', { method: 'POST' });
const data = await response.json();
if (data.success) {
document.getElementById('health-check-status').textContent =
`Last check: ${new Date(data.lastCheck).toLocaleString()} (${data.containersMonitored} containers)`;
showNotification('Health check completed', 'success', 2000);
}
} catch (error) {
showNotification(`Error: ${error.message}`, 'error', 3000);
}
});
// Modal handlers
openBtn?.addEventListener('click', () => {
modal.classList.add('show');
loadNotificationConfig();
loadNotificationHistory();
});
saveBtn?.addEventListener('click', saveNotificationConfig);
// Send Test button - sends test notification to all enabled providers
document.getElementById('notifications-send-test')?.addEventListener('click', async () => {
const btn = document.getElementById('notifications-send-test');
const originalText = btn.textContent;
btn.textContent = 'Sending...';
btn.disabled = true;
try {
const response = await secureFetch('/api/v1/notifications/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
event: 'test',
data: { message: 'This is a test notification from DashCaddy.' },
type: 'info'
})
});
const data = await response.json();
if (data.success) {
showNotification('Test notification sent!', 'success', 3000);
// Update last sent timestamp
loadNotificationStatus();
} else {
showNotification(`Test failed: ${data.results?.map(r => `${r.provider}: ${r.error || 'ok'}`).join(', ')}`, 'error', 5000);
}
} catch (error) {
showNotification(`Error: ${error.message}`, 'error', 3000);
} finally {
btn.textContent = originalText;
btn.disabled = false;
}
});
// Load notification status (last sent timestamp)
async function loadNotificationStatus() {
try {
const response = await fetch('/api/v1/notifications/status');
const data = await response.json();
if (data.success && data.lastSent) {
const lastSentEl = document.getElementById('last-notification-sent');
if (lastSentEl) {
lastSentEl.textContent = `Last sent: ${new Date(data.lastSent).toLocaleString()}`;
}
}
} catch (error) {
// Silently fail - status is not critical
}
}
wireModal(modal, cancelBtn);
})();