added webhooks, moved app name and verison to simple config file
This commit is contained in:
369
app/web/templates/webhooks/form.html
Normal file
369
app/web/templates/webhooks/form.html
Normal 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 %}
|
||||
250
app/web/templates/webhooks/list.html
Normal file
250
app/web/templates/webhooks/list.html
Normal 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 %}
|
||||
328
app/web/templates/webhooks/logs.html
Normal file
328
app/web/templates/webhooks/logs.html
Normal 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, '"')})">
|
||||
<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 %}
|
||||
Reference in New Issue
Block a user