Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
344
status/js/notification-settings.js
Normal file
344
status/js/notification-settings.js
Normal file
@@ -0,0 +1,344 @@
|
||||
// ========== NOTIFICATION SETTINGS ==========
|
||||
(function() {
|
||||
// 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>
|
||||
|
||||
<!-- 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>
|
||||
</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>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="weather-modal-buttons modal-footer-bar">
|
||||
<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'].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;
|
||||
|
||||
// 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';
|
||||
|
||||
// ntfy server URL
|
||||
if (config.providers?.ntfy?.serverUrl) {
|
||||
document.getElementById('ntfy-server').value = config.providers.ntfy.serverUrl;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load notification config:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
console.error('Failed to load notification history:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
},
|
||||
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
|
||||
},
|
||||
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'));
|
||||
|
||||
// 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);
|
||||
wireModal(modal, cancelBtn);
|
||||
})();
|
||||
Reference in New Issue
Block a user