added webhooks, moved app name and verison to simple config file

This commit is contained in:
2025-11-18 15:05:57 -06:00
parent 1d076a467a
commit 28b32a2049
12 changed files with 2041 additions and 0 deletions

View File

@@ -0,0 +1,369 @@
{% extends "base.html" %}
{% block title %}{{ 'Edit' if mode == 'edit' else 'New' }} Webhook - SneakyScanner{% endblock %}
{% block content %}
<div class="row mt-4">
<div class="col-12 mb-4">
<h1 style="color: #60a5fa;">{{ 'Edit' if mode == 'edit' else 'Create' }} Webhook</h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('webhooks.list_webhooks') }}">Webhooks</a></li>
<li class="breadcrumb-item active">{{ 'Edit' if mode == 'edit' else 'New' }}</li>
</ol>
</nav>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-body">
<form id="webhook-form">
<!-- Basic Information -->
<h5 class="card-title mb-3">Basic Information</h5>
<div class="mb-3">
<label for="name" class="form-label">Webhook Name <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="name" name="name" required
placeholder="e.g., Slack Notifications">
<div class="form-text">A descriptive name for this webhook</div>
</div>
<div class="mb-3">
<label for="url" class="form-label">Webhook URL <span class="text-danger">*</span></label>
<input type="url" class="form-control" id="url" name="url" required
placeholder="https://hooks.example.com/webhook">
<div class="form-text">The endpoint where alerts will be sent</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="enabled" name="enabled" checked>
<label class="form-check-label" for="enabled">Enabled</label>
</div>
<div class="form-text">Disabled webhooks will not receive notifications</div>
</div>
<hr class="my-4">
<!-- Authentication -->
<h5 class="card-title mb-3">Authentication</h5>
<div class="mb-3">
<label for="auth_type" class="form-label">Authentication Type</label>
<select class="form-select" id="auth_type" name="auth_type">
<option value="none">None</option>
<option value="bearer">Bearer Token</option>
<option value="basic">Basic Auth (username:password)</option>
<option value="custom">Custom Headers</option>
</select>
</div>
<div class="mb-3" id="auth_token_field" style="display: none;">
<label for="auth_token" class="form-label">Authentication Token</label>
<input type="password" class="form-control" id="auth_token" name="auth_token"
placeholder="Enter token or username:password">
<div class="form-text" id="auth_token_help">Will be encrypted when stored</div>
</div>
<div class="mb-3" id="custom_headers_field" style="display: none;">
<label for="custom_headers" class="form-label">Custom Headers (JSON)</label>
<textarea class="form-control font-monospace" id="custom_headers" name="custom_headers" rows="4"
placeholder='{"X-API-Key": "your-key", "X-Custom-Header": "value"}'></textarea>
<div class="form-text">JSON object with custom HTTP headers</div>
</div>
<hr class="my-4">
<!-- Filters -->
<h5 class="card-title mb-3">Alert Filters</h5>
<div class="mb-3">
<label class="form-label">Alert Types</label>
<div class="form-text mb-2">Select which alert types trigger this webhook (leave all unchecked for all types)</div>
<div class="form-check">
<input class="form-check-input alert-type-check" type="checkbox" value="unexpected_port" id="type_unexpected">
<label class="form-check-label" for="type_unexpected">Unexpected Port</label>
</div>
<div class="form-check">
<input class="form-check-input alert-type-check" type="checkbox" value="drift_detection" id="type_drift">
<label class="form-check-label" for="type_drift">Drift Detection</label>
</div>
<div class="form-check">
<input class="form-check-input alert-type-check" type="checkbox" value="cert_expiry" id="type_cert">
<label class="form-check-label" for="type_cert">Certificate Expiry</label>
</div>
<div class="form-check">
<input class="form-check-input alert-type-check" type="checkbox" value="weak_tls" id="type_tls">
<label class="form-check-label" for="type_tls">Weak TLS</label>
</div>
<div class="form-check">
<input class="form-check-input alert-type-check" type="checkbox" value="ping_failed" id="type_ping">
<label class="form-check-label" for="type_ping">Ping Failed</label>
</div>
</div>
<div class="mb-3">
<label class="form-label">Severity Filter</label>
<div class="form-text mb-2">Select which severities trigger this webhook (leave all unchecked for all severities)</div>
<div class="form-check">
<input class="form-check-input severity-check" type="checkbox" value="critical" id="severity_critical">
<label class="form-check-label" for="severity_critical">Critical</label>
</div>
<div class="form-check">
<input class="form-check-input severity-check" type="checkbox" value="warning" id="severity_warning">
<label class="form-check-label" for="severity_warning">Warning</label>
</div>
<div class="form-check">
<input class="form-check-input severity-check" type="checkbox" value="info" id="severity_info">
<label class="form-check-label" for="severity_info">Info</label>
</div>
</div>
<hr class="my-4">
<!-- Advanced Settings -->
<h5 class="card-title mb-3">Advanced Settings</h5>
<div class="row">
<div class="col-md-6 mb-3">
<label for="timeout" class="form-label">Timeout (seconds)</label>
<input type="number" class="form-control" id="timeout" name="timeout" min="1" max="60" value="10">
<div class="form-text">Maximum time to wait for response</div>
</div>
<div class="col-md-6 mb-3">
<label for="retry_count" class="form-label">Retry Count</label>
<input type="number" class="form-control" id="retry_count" name="retry_count" min="0" max="5" value="3">
<div class="form-text">Number of retry attempts on failure</div>
</div>
</div>
<hr class="my-4">
<!-- Submit Buttons -->
<div class="d-flex justify-content-between">
<a href="{{ url_for('webhooks.list_webhooks') }}" class="btn btn-secondary">Cancel</a>
<div>
<button type="button" class="btn btn-outline-primary me-2" id="test-btn">
<i class="bi bi-send"></i> Test Webhook
</button>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> Save Webhook
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- Help Sidebar -->
<div class="col-lg-4">
<div class="card">
<div class="card-body">
<h5 class="card-title"><i class="bi bi-info-circle"></i> Help</h5>
<h6 class="mt-3">Payload Format</h6>
<p class="small text-muted">Webhooks receive JSON payloads with alert details:</p>
<pre class="small bg-dark text-light p-2 rounded"><code>{
"event": "alert.created",
"alert": {
"id": 123,
"type": "cert_expiry",
"severity": "warning",
"message": "...",
"ip_address": "192.168.1.10",
"port": 443
},
"scan": {...},
"rule": {...}
}</code></pre>
<h6 class="mt-3">Authentication Types</h6>
<ul class="small">
<li><strong>None:</strong> No authentication</li>
<li><strong>Bearer:</strong> Add Authorization header with token</li>
<li><strong>Basic:</strong> Use username:password format</li>
<li><strong>Custom:</strong> Define custom HTTP headers</li>
</ul>
<h6 class="mt-3">Retry Logic</h6>
<p class="small text-muted">Failed webhooks are retried with exponential backoff (2^attempt seconds, max 60s).</p>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const webhookId = {{ webhook.id if webhook else 'null' }};
const mode = '{{ mode }}';
// Show/hide auth fields based on type
document.getElementById('auth_type').addEventListener('change', function() {
const authType = this.value;
const tokenField = document.getElementById('auth_token_field');
const headersField = document.getElementById('custom_headers_field');
const tokenHelp = document.getElementById('auth_token_help');
tokenField.style.display = 'none';
headersField.style.display = 'none';
if (authType === 'bearer') {
tokenField.style.display = 'block';
document.getElementById('auth_token').placeholder = 'Enter bearer token';
tokenHelp.textContent = 'Bearer token for Authorization header (encrypted when stored)';
} else if (authType === 'basic') {
tokenField.style.display = 'block';
document.getElementById('auth_token').placeholder = 'username:password';
tokenHelp.textContent = 'Format: username:password (encrypted when stored)';
} else if (authType === 'custom') {
headersField.style.display = 'block';
}
});
// Load existing webhook data if editing
if (mode === 'edit' && webhookId) {
loadWebhookData(webhookId);
}
async function loadWebhookData(id) {
try {
const response = await fetch(`/api/webhooks/${id}`);
const data = await response.json();
const webhook = data.webhook;
// Populate form fields
document.getElementById('name').value = webhook.name || '';
document.getElementById('url').value = webhook.url || '';
document.getElementById('enabled').checked = webhook.enabled;
document.getElementById('auth_type').value = webhook.auth_type || 'none';
document.getElementById('timeout').value = webhook.timeout || 10;
document.getElementById('retry_count').value = webhook.retry_count || 3;
// Trigger auth type change to show relevant fields
document.getElementById('auth_type').dispatchEvent(new Event('change'));
// Don't populate auth_token (it's encrypted)
if (webhook.custom_headers) {
document.getElementById('custom_headers').value = JSON.stringify(webhook.custom_headers, null, 2);
}
// Check alert types
if (webhook.alert_types && webhook.alert_types.length > 0) {
webhook.alert_types.forEach(type => {
const checkbox = document.querySelector(`.alert-type-check[value="${type}"]`);
if (checkbox) checkbox.checked = true;
});
}
// Check severities
if (webhook.severity_filter && webhook.severity_filter.length > 0) {
webhook.severity_filter.forEach(sev => {
const checkbox = document.querySelector(`.severity-check[value="${sev}"]`);
if (checkbox) checkbox.checked = true;
});
}
} catch (error) {
console.error('Error loading webhook:', error);
alert('Failed to load webhook data');
}
}
// Form submission
document.getElementById('webhook-form').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = {
name: document.getElementById('name').value,
url: document.getElementById('url').value,
enabled: document.getElementById('enabled').checked,
auth_type: document.getElementById('auth_type').value,
timeout: parseInt(document.getElementById('timeout').value),
retry_count: parseInt(document.getElementById('retry_count').value)
};
// Add auth token if provided
const authToken = document.getElementById('auth_token').value;
if (authToken) {
formData.auth_token = authToken;
}
// Add custom headers if provided
const customHeaders = document.getElementById('custom_headers').value;
if (customHeaders.trim()) {
try {
formData.custom_headers = JSON.parse(customHeaders);
} catch (e) {
alert('Invalid JSON in custom headers');
return;
}
}
// Collect selected alert types
const alertTypes = Array.from(document.querySelectorAll('.alert-type-check:checked'))
.map(cb => cb.value);
if (alertTypes.length > 0) {
formData.alert_types = alertTypes;
}
// Collect selected severities
const severities = Array.from(document.querySelectorAll('.severity-check:checked'))
.map(cb => cb.value);
if (severities.length > 0) {
formData.severity_filter = severities;
}
try {
const url = mode === 'edit' ? `/api/webhooks/${webhookId}` : '/api/webhooks';
const method = mode === 'edit' ? 'PUT' : 'POST';
const response = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.status === 'success') {
alert('Webhook saved successfully!');
window.location.href = '{{ url_for("webhooks.list_webhooks") }}';
} else {
alert(`Failed to save webhook: ${result.message}`);
}
} catch (error) {
console.error('Error saving webhook:', error);
alert('Failed to save webhook');
}
});
// Test webhook button
document.getElementById('test-btn').addEventListener('click', async function() {
if (mode !== 'edit' || !webhookId) {
alert('Please save the webhook first before testing');
return;
}
if (!confirm('Send a test payload to this webhook?')) return;
try {
const response = await fetch(`/api/webhooks/${webhookId}/test`, { method: 'POST' });
const result = await response.json();
if (result.status === 'success') {
alert(`Test successful!\nHTTP ${result.status_code}\n${result.message}`);
} else {
alert(`Test failed:\n${result.message}`);
}
} catch (error) {
console.error('Error testing webhook:', error);
alert('Failed to test webhook');
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,250 @@
{% extends "base.html" %}
{% block title %}Webhooks - SneakyScanner{% endblock %}
{% block content %}
<div class="row mt-4">
<div class="col-12 d-flex justify-content-between align-items-center mb-4">
<h1 style="color: #60a5fa;">Webhook Management</h1>
<a href="{{ url_for('webhooks.new_webhook') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add Webhook
</a>
</div>
</div>
<!-- Loading indicator -->
<div id="loading" class="text-center my-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<!-- Webhooks table -->
<div id="webhooks-container" style="display: none;">
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>URL</th>
<th>Alert Types</th>
<th>Severity Filter</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="webhooks-tbody">
<!-- Populated via JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Pagination -->
<nav aria-label="Webhooks pagination" id="pagination-container">
<ul class="pagination justify-content-center" id="pagination">
<!-- Populated via JavaScript -->
</ul>
</nav>
</div>
<!-- Empty state -->
<div id="empty-state" class="text-center my-5" style="display: none;">
<i class="bi bi-webhook" style="font-size: 4rem; color: #94a3b8;"></i>
<p class="text-muted mt-3">No webhooks configured yet.</p>
<a href="{{ url_for('webhooks.new_webhook') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Create Your First Webhook
</a>
</div>
{% endblock %}
{% block scripts %}
<script>
let currentPage = 1;
const perPage = 20;
async function loadWebhooks(page = 1) {
try {
const response = await fetch(`/api/webhooks?page=${page}&per_page=${perPage}`);
const data = await response.json();
if (data.webhooks && data.webhooks.length > 0) {
renderWebhooks(data.webhooks);
renderPagination(data.page, data.pages, data.total);
document.getElementById('webhooks-container').style.display = 'block';
document.getElementById('empty-state').style.display = 'none';
} else {
document.getElementById('webhooks-container').style.display = 'none';
document.getElementById('empty-state').style.display = 'block';
}
} catch (error) {
console.error('Error loading webhooks:', error);
alert('Failed to load webhooks');
} finally {
document.getElementById('loading').style.display = 'none';
}
}
function renderWebhooks(webhooks) {
const tbody = document.getElementById('webhooks-tbody');
tbody.innerHTML = '';
webhooks.forEach(webhook => {
const row = document.createElement('tr');
// Truncate URL for display
const truncatedUrl = webhook.url.length > 50 ?
webhook.url.substring(0, 47) + '...' : webhook.url;
// Format alert types
const alertTypes = webhook.alert_types && webhook.alert_types.length > 0 ?
webhook.alert_types.map(t => `<span class="badge bg-secondary me-1">${t}</span>`).join('') :
'<span class="text-muted">All</span>';
// Format severity filter
const severityFilter = webhook.severity_filter && webhook.severity_filter.length > 0 ?
webhook.severity_filter.map(s => `<span class="badge bg-${getSeverityColor(s)} me-1">${s}</span>`).join('') :
'<span class="text-muted">All</span>';
// Status badge
const statusBadge = webhook.enabled ?
'<span class="badge bg-success">Enabled</span>' :
'<span class="badge bg-secondary">Disabled</span>';
row.innerHTML = `
<td><strong>${escapeHtml(webhook.name)}</strong></td>
<td><code class="small">${escapeHtml(truncatedUrl)}</code></td>
<td>${alertTypes}</td>
<td>${severityFilter}</td>
<td>${statusBadge}</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-primary" onclick="testWebhook(${webhook.id})" title="Test">
<i class="bi bi-send"></i>
</button>
<a href="/webhooks/${webhook.id}/edit" class="btn btn-outline-primary" title="Edit">
<i class="bi bi-pencil"></i>
</a>
<a href="/webhooks/${webhook.id}/logs" class="btn btn-outline-info" title="Logs">
<i class="bi bi-list-ul"></i>
</a>
<button class="btn btn-outline-danger" onclick="deleteWebhook(${webhook.id}, '${escapeHtml(webhook.name)}')" title="Delete">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
`;
tbody.appendChild(row);
});
}
function renderPagination(currentPage, totalPages, totalItems) {
const pagination = document.getElementById('pagination');
pagination.innerHTML = '';
if (totalPages <= 1) {
document.getElementById('pagination-container').style.display = 'none';
return;
}
document.getElementById('pagination-container').style.display = 'block';
// Previous button
const prevLi = document.createElement('li');
prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${currentPage - 1}); return false;">Previous</a>`;
pagination.appendChild(prevLi);
// Page numbers
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
const li = document.createElement('li');
li.className = `page-item ${i === currentPage ? 'active' : ''}`;
li.innerHTML = `<a class="page-link" href="#" onclick="changePage(${i}); return false;">${i}</a>`;
pagination.appendChild(li);
} else if (i === currentPage - 3 || i === currentPage + 3) {
const li = document.createElement('li');
li.className = 'page-item disabled';
li.innerHTML = '<a class="page-link" href="#">...</a>';
pagination.appendChild(li);
}
}
// Next button
const nextLi = document.createElement('li');
nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
nextLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${currentPage + 1}); return false;">Next</a>`;
pagination.appendChild(nextLi);
}
function changePage(page) {
currentPage = page;
loadWebhooks(page);
}
async function testWebhook(id) {
if (!confirm('Send a test payload to this webhook?')) return;
try {
const response = await fetch(`/api/webhooks/${id}/test`, { method: 'POST' });
const result = await response.json();
if (result.status === 'success') {
alert(`Test successful!\nHTTP ${result.status_code}\n${result.message}`);
} else {
alert(`Test failed:\n${result.message}`);
}
} catch (error) {
console.error('Error testing webhook:', error);
alert('Failed to test webhook');
}
}
async function deleteWebhook(id, name) {
if (!confirm(`Are you sure you want to delete webhook "${name}"?`)) return;
try {
const response = await fetch(`/api/webhooks/${id}`, { method: 'DELETE' });
const result = await response.json();
if (result.status === 'success') {
alert('Webhook deleted successfully');
loadWebhooks(currentPage);
} else {
alert(`Failed to delete webhook: ${result.message}`);
}
} catch (error) {
console.error('Error deleting webhook:', error);
alert('Failed to delete webhook');
}
}
function getSeverityColor(severity) {
const colors = {
'critical': 'danger',
'warning': 'warning',
'info': 'info'
};
return colors[severity] || 'secondary';
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Load webhooks on page load
document.addEventListener('DOMContentLoaded', () => {
loadWebhooks(1);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,328 @@
{% extends "base.html" %}
{% block title %}Webhook Logs - {{ webhook.name }} - SneakyScanner{% endblock %}
{% block content %}
<div class="row mt-4">
<div class="col-12 mb-4">
<h1 style="color: #60a5fa;">Webhook Delivery Logs</h1>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('webhooks.list_webhooks') }}">Webhooks</a></li>
<li class="breadcrumb-item active">{{ webhook.name }}</li>
</ol>
</nav>
</div>
</div>
<!-- Webhook Info -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h5 class="card-title">{{ webhook.name }}</h5>
<p class="text-muted mb-1"><strong>URL:</strong> <code>{{ webhook.url }}</code></p>
<p class="text-muted mb-0">
<strong>Status:</strong>
{% if webhook.enabled %}
<span class="badge bg-success">Enabled</span>
{% else %}
<span class="badge bg-secondary">Disabled</span>
{% endif %}
</p>
</div>
<div class="col-md-6 text-md-end">
<a href="{{ url_for('webhooks.edit_webhook', webhook_id=webhook.id) }}" class="btn btn-primary">
<i class="bi bi-pencil"></i> Edit Webhook
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label for="status-filter" class="form-label">Status</label>
<select class="form-select" id="status-filter">
<option value="">All</option>
<option value="success">Success</option>
<option value="failed">Failed</option>
</select>
</div>
<div class="col-md-4 d-flex align-items-end">
<button type="button" class="btn btn-primary w-100" onclick="applyFilter()">
<i class="bi bi-funnel"></i> Apply Filter
</button>
</div>
<div class="col-md-4 d-flex align-items-end">
<button type="button" class="btn btn-outline-secondary w-100" onclick="refreshLogs()">
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Loading indicator -->
<div id="loading" class="text-center my-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<!-- Logs table -->
<div id="logs-container" style="display: none;">
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Timestamp</th>
<th>Alert</th>
<th>Status</th>
<th>HTTP Code</th>
<th>Attempt</th>
<th>Details</th>
</tr>
</thead>
<tbody id="logs-tbody">
<!-- Populated via JavaScript -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Pagination -->
<nav aria-label="Logs pagination" id="pagination-container">
<ul class="pagination justify-content-center" id="pagination">
<!-- Populated via JavaScript -->
</ul>
</nav>
</div>
<!-- Empty state -->
<div id="empty-state" class="text-center my-5" style="display: none;">
<i class="bi bi-list-ul" style="font-size: 4rem; color: #94a3b8;"></i>
<p class="text-muted mt-3">No delivery logs yet.</p>
<p class="small text-muted">Logs will appear here after alerts trigger this webhook.</p>
</div>
<!-- Modal for log details -->
<div class="modal fade" id="logDetailModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delivery Log Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="modal-content">
<!-- Populated via JavaScript -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const webhookId = {{ webhook.id }};
let currentPage = 1;
let currentStatus = '';
const perPage = 20;
async function loadLogs(page = 1, status = '') {
try {
let url = `/api/webhooks/${webhookId}/logs?page=${page}&per_page=${perPage}`;
if (status) {
url += `&status=${status}`;
}
const response = await fetch(url);
const data = await response.json();
if (data.logs && data.logs.length > 0) {
renderLogs(data.logs);
renderPagination(data.page, data.pages, data.total);
document.getElementById('logs-container').style.display = 'block';
document.getElementById('empty-state').style.display = 'none';
} else {
document.getElementById('logs-container').style.display = 'none';
document.getElementById('empty-state').style.display = 'block';
}
} catch (error) {
console.error('Error loading logs:', error);
alert('Failed to load delivery logs');
} finally {
document.getElementById('loading').style.display = 'none';
}
}
function renderLogs(logs) {
const tbody = document.getElementById('logs-tbody');
tbody.innerHTML = '';
logs.forEach(log => {
const row = document.createElement('tr');
// Format timestamp
const timestamp = new Date(log.delivered_at).toLocaleString();
// Status badge
const statusBadge = log.status === 'success' ?
'<span class="badge bg-success">Success</span>' :
'<span class="badge bg-danger">Failed</span>';
// HTTP code badge
const httpBadge = log.response_code ?
`<span class="badge ${log.response_code < 400 ? 'bg-success' : 'bg-danger'}">${log.response_code}</span>` :
'<span class="text-muted">N/A</span>';
// Alert info
const alertInfo = log.alert_type ?
`<span class="badge bg-secondary">${log.alert_type}</span><br><small class="text-muted">${escapeHtml(log.alert_message || '')}</small>` :
`<small class="text-muted">Alert #${log.alert_id}</small>`;
row.innerHTML = `
<td><small>${timestamp}</small></td>
<td>${alertInfo}</td>
<td>${statusBadge}</td>
<td>${httpBadge}</td>
<td>${log.attempt_number || 1}</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="showLogDetails(${JSON.stringify(log).replace(/"/g, '&quot;')})">
<i class="bi bi-eye"></i> View
</button>
</td>
`;
tbody.appendChild(row);
});
}
function renderPagination(currentPage, totalPages, totalItems) {
const pagination = document.getElementById('pagination');
pagination.innerHTML = '';
if (totalPages <= 1) {
document.getElementById('pagination-container').style.display = 'none';
return;
}
document.getElementById('pagination-container').style.display = 'block';
// Previous button
const prevLi = document.createElement('li');
prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${currentPage - 1}); return false;">Previous</a>`;
pagination.appendChild(prevLi);
// Page numbers
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) {
const li = document.createElement('li');
li.className = `page-item ${i === currentPage ? 'active' : ''}`;
li.innerHTML = `<a class="page-link" href="#" onclick="changePage(${i}); return false;">${i}</a>`;
pagination.appendChild(li);
} else if (i === currentPage - 3 || i === currentPage + 3) {
const li = document.createElement('li');
li.className = 'page-item disabled';
li.innerHTML = '<a class="page-link" href="#">...</a>';
pagination.appendChild(li);
}
}
// Next button
const nextLi = document.createElement('li');
nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
nextLi.innerHTML = `<a class="page-link" href="#" onclick="changePage(${currentPage + 1}); return false;">Next</a>`;
pagination.appendChild(nextLi);
}
function changePage(page) {
currentPage = page;
loadLogs(page, currentStatus);
}
function applyFilter() {
currentStatus = document.getElementById('status-filter').value;
currentPage = 1;
loadLogs(1, currentStatus);
}
function refreshLogs() {
loadLogs(currentPage, currentStatus);
}
function showLogDetails(log) {
const modalContent = document.getElementById('modal-content');
let detailsHTML = `
<div class="mb-3">
<strong>Log ID:</strong> ${log.id}<br>
<strong>Alert ID:</strong> ${log.alert_id}<br>
<strong>Status:</strong> <span class="badge ${log.status === 'success' ? 'bg-success' : 'bg-danger'}">${log.status}</span><br>
<strong>HTTP Code:</strong> ${log.response_code || 'N/A'}<br>
<strong>Attempt:</strong> ${log.attempt_number || 1}<br>
<strong>Delivered At:</strong> ${new Date(log.delivered_at).toLocaleString()}
</div>
`;
if (log.response_body) {
detailsHTML += `
<div class="mb-3">
<strong>Response Body:</strong>
<pre class="bg-dark text-light p-2 rounded mt-2"><code>${escapeHtml(log.response_body)}</code></pre>
</div>
`;
}
if (log.error_message) {
detailsHTML += `
<div class="mb-3">
<strong>Error Message:</strong>
<div class="alert alert-danger mt-2">${escapeHtml(log.error_message)}</div>
</div>
`;
}
modalContent.innerHTML = detailsHTML;
const modal = new bootstrap.Modal(document.getElementById('logDetailModal'));
modal.show();
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Load logs on page load
document.addEventListener('DOMContentLoaded', () => {
loadLogs(1);
});
</script>
{% endblock %}