581 lines
23 KiB
HTML
581 lines
23 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Scan Configurations - SneakyScanner{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row mt-4">
|
|
<div class="col-12">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h1 style="color: #60a5fa;">Scan Configurations</h1>
|
|
<div>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createConfigModal">
|
|
<i class="bi bi-plus-circle"></i> Create New Config
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary Stats -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-4">
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="total-configs">-</div>
|
|
<div class="stat-label">Total Configs</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="total-sites-used">-</div>
|
|
<div class="stat-label">Total Sites Referenced</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="recent-updates">-</div>
|
|
<div class="stat-label">Updated This Week</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Configs Table -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0" style="color: #60a5fa;">All Configurations</h5>
|
|
<input type="text" id="search-input" class="form-control" style="max-width: 300px;"
|
|
placeholder="Search configs...">
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="configs-loading" class="text-center py-5">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-3 text-muted">Loading configurations...</p>
|
|
</div>
|
|
<div id="configs-error" style="display: none;" class="alert alert-danger">
|
|
<strong>Error:</strong> <span id="error-message"></span>
|
|
</div>
|
|
<div id="configs-content" style="display: none;">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Title</th>
|
|
<th>Description</th>
|
|
<th>Sites</th>
|
|
<th>Updated</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="configs-tbody">
|
|
<!-- Populated by JavaScript -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div id="empty-state" style="display: none;" class="text-center py-5">
|
|
<i class="bi bi-gear" style="font-size: 3rem; color: #64748b;"></i>
|
|
<h5 class="mt-3 text-muted">No configurations defined</h5>
|
|
<p class="text-muted">Create your first scan configuration</p>
|
|
<button class="btn btn-primary mt-2" data-bs-toggle="modal" data-bs-target="#createConfigModal">
|
|
<i class="bi bi-plus-circle"></i> Create Config
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Config Modal -->
|
|
<div class="modal fade" id="createConfigModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content" style="background-color: #1e293b; border: 1px solid #334155;">
|
|
<div class="modal-header" style="border-bottom: 1px solid #334155;">
|
|
<h5 class="modal-title" style="color: #60a5fa;">
|
|
<i class="bi bi-plus-circle"></i> Create New Configuration
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="create-config-form">
|
|
<div class="mb-3">
|
|
<label for="config-title" class="form-label">Title <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="config-title" required
|
|
placeholder="e.g., Production Weekly Scan">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="config-description" class="form-label">Description</label>
|
|
<textarea class="form-control" id="config-description" rows="3"
|
|
placeholder="Optional description of this configuration"></textarea>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Sites <span class="text-danger">*</span></label>
|
|
<div id="sites-loading-modal" class="text-center py-3">
|
|
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<span class="ms-2 text-muted">Loading available sites...</span>
|
|
</div>
|
|
<div id="sites-list" style="display: none;">
|
|
<!-- Populated by JavaScript -->
|
|
</div>
|
|
<small class="form-text text-muted">Select at least one site to include in this configuration</small>
|
|
</div>
|
|
|
|
<div class="alert alert-danger" id="create-config-error" style="display: none;">
|
|
<span id="create-config-error-message"></span>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer" style="border-top: 1px solid #334155;">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" id="create-config-btn">
|
|
<i class="bi bi-check-circle"></i> Create Configuration
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Config Modal -->
|
|
<div class="modal fade" id="editConfigModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content" style="background-color: #1e293b; border: 1px solid #334155;">
|
|
<div class="modal-header" style="border-bottom: 1px solid #334155;">
|
|
<h5 class="modal-title" style="color: #60a5fa;">
|
|
<i class="bi bi-pencil"></i> Edit Configuration
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="edit-config-form">
|
|
<input type="hidden" id="edit-config-id">
|
|
|
|
<div class="mb-3">
|
|
<label for="edit-config-title" class="form-label">Title <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" id="edit-config-title" required>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="edit-config-description" class="form-label">Description</label>
|
|
<textarea class="form-control" id="edit-config-description" rows="3"></textarea>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Sites <span class="text-danger">*</span></label>
|
|
<div id="edit-sites-list">
|
|
<!-- Populated by JavaScript -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="alert alert-danger" id="edit-config-error" style="display: none;">
|
|
<span id="edit-config-error-message"></span>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer" style="border-top: 1px solid #334155;">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" id="edit-config-btn">
|
|
<i class="bi bi-check-circle"></i> Save Changes
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- View Config Modal -->
|
|
<div class="modal fade" id="viewConfigModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content" style="background-color: #1e293b; border: 1px solid #334155;">
|
|
<div class="modal-header" style="border-bottom: 1px solid #334155;">
|
|
<h5 class="modal-title" style="color: #60a5fa;">
|
|
<i class="bi bi-eye"></i> Configuration Details
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="view-config-content">
|
|
<!-- Populated by JavaScript -->
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer" style="border-top: 1px solid #334155;">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
<div class="modal fade" id="deleteConfigModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content" style="background-color: #1e293b; border: 1px solid #334155;">
|
|
<div class="modal-header" style="border-bottom: 1px solid #334155;">
|
|
<h5 class="modal-title" style="color: #ef4444;">
|
|
<i class="bi bi-trash"></i> Delete Configuration
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Are you sure you want to delete configuration <strong id="delete-config-name"></strong>?</p>
|
|
<p class="text-warning"><i class="bi bi-exclamation-triangle"></i> This action cannot be undone.</p>
|
|
<input type="hidden" id="delete-config-id">
|
|
</div>
|
|
<div class="modal-footer" style="border-top: 1px solid #334155;">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-danger" id="confirm-delete-btn">
|
|
<i class="bi bi-trash"></i> Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
// Global state
|
|
let allConfigs = [];
|
|
let allSites = [];
|
|
|
|
// Load data on page load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadSites();
|
|
loadConfigs();
|
|
});
|
|
|
|
// Load all sites
|
|
async function loadSites() {
|
|
try {
|
|
const response = await fetch('/api/sites?all=true');
|
|
if (!response.ok) throw new Error('Failed to load sites');
|
|
|
|
const data = await response.json();
|
|
allSites = data.sites || [];
|
|
|
|
renderSitesCheckboxes();
|
|
} catch (error) {
|
|
console.error('Error loading sites:', error);
|
|
document.getElementById('sites-loading-modal').innerHTML =
|
|
'<div class="alert alert-danger">Failed to load sites</div>';
|
|
}
|
|
}
|
|
|
|
// Render sites checkboxes
|
|
function renderSitesCheckboxes(selectedIds = [], isEditMode = false) {
|
|
const container = isEditMode ? document.getElementById('edit-sites-list') : document.getElementById('sites-list');
|
|
|
|
if (!container) return;
|
|
|
|
if (allSites.length === 0) {
|
|
const message = '<div class="alert alert-info">No sites available. <a href="/sites">Create a site first</a>.</div>';
|
|
container.innerHTML = message;
|
|
if (!isEditMode) {
|
|
document.getElementById('sites-loading-modal').style.display = 'none';
|
|
container.style.display = 'block';
|
|
}
|
|
return;
|
|
}
|
|
|
|
const prefix = isEditMode ? 'edit-site' : 'site';
|
|
const checkboxClass = isEditMode ? 'edit-site-checkbox' : 'site-checkbox';
|
|
|
|
let html = '<div style="max-height: 300px; overflow-y: auto;">';
|
|
allSites.forEach(site => {
|
|
const isChecked = selectedIds.includes(site.id);
|
|
html += `
|
|
<div class="form-check">
|
|
<input class="form-check-input ${checkboxClass}" type="checkbox" value="${site.id}"
|
|
id="${prefix}-${site.id}" ${isChecked ? 'checked' : ''}>
|
|
<label class="form-check-label" for="${prefix}-${site.id}">
|
|
${escapeHtml(site.name)}
|
|
<small class="text-muted">(${site.cidr_count || 0} CIDR${site.cidr_count !== 1 ? 's' : ''})</small>
|
|
</label>
|
|
</div>
|
|
`;
|
|
});
|
|
html += '</div>';
|
|
|
|
container.innerHTML = html;
|
|
|
|
if (!isEditMode) {
|
|
document.getElementById('sites-loading-modal').style.display = 'none';
|
|
container.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// Load all configs
|
|
async function loadConfigs() {
|
|
try {
|
|
const response = await fetch('/api/configs');
|
|
if (!response.ok) throw new Error('Failed to load configs');
|
|
|
|
const data = await response.json();
|
|
allConfigs = data.configs || [];
|
|
|
|
renderConfigs();
|
|
updateStats();
|
|
|
|
document.getElementById('configs-loading').style.display = 'none';
|
|
document.getElementById('configs-content').style.display = 'block';
|
|
} catch (error) {
|
|
console.error('Error loading configs:', error);
|
|
document.getElementById('configs-loading').style.display = 'none';
|
|
document.getElementById('configs-error').style.display = 'block';
|
|
document.getElementById('error-message').textContent = error.message;
|
|
}
|
|
}
|
|
|
|
// Render configs table
|
|
function renderConfigs(filter = '') {
|
|
const tbody = document.getElementById('configs-tbody');
|
|
const emptyState = document.getElementById('empty-state');
|
|
|
|
const filteredConfigs = filter
|
|
? allConfigs.filter(c =>
|
|
c.title.toLowerCase().includes(filter.toLowerCase()) ||
|
|
(c.description && c.description.toLowerCase().includes(filter.toLowerCase()))
|
|
)
|
|
: allConfigs;
|
|
|
|
if (filteredConfigs.length === 0) {
|
|
tbody.innerHTML = '';
|
|
emptyState.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
emptyState.style.display = 'none';
|
|
tbody.innerHTML = filteredConfigs.map(config => `
|
|
<tr>
|
|
<td><strong>${escapeHtml(config.title)}</strong></td>
|
|
<td>${config.description ? escapeHtml(config.description) : '<span class="text-muted">-</span>'}</td>
|
|
<td>
|
|
<span class="badge bg-primary">${config.site_count} site${config.site_count !== 1 ? 's' : ''}</span>
|
|
</td>
|
|
<td>${formatDate(config.updated_at)}</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-info" onclick="viewConfig(${config.id})" title="View">
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-warning" onclick="editConfig(${config.id})" title="Edit">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
<button class="btn btn-sm btn-danger" onclick="deleteConfig(${config.id}, '${escapeHtml(config.title).replace(/'/g, "\\'")}');" title="Delete">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
// Update stats
|
|
function updateStats() {
|
|
document.getElementById('total-configs').textContent = allConfigs.length;
|
|
|
|
const uniqueSites = new Set();
|
|
allConfigs.forEach(c => c.sites.forEach(s => uniqueSites.add(s.id)));
|
|
document.getElementById('total-sites-used').textContent = uniqueSites.size;
|
|
|
|
const oneWeekAgo = new Date();
|
|
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
const recentUpdates = allConfigs.filter(c => new Date(c.updated_at) > oneWeekAgo).length;
|
|
document.getElementById('recent-updates').textContent = recentUpdates;
|
|
}
|
|
|
|
// Search functionality
|
|
document.getElementById('search-input').addEventListener('input', function(e) {
|
|
renderConfigs(e.target.value);
|
|
});
|
|
|
|
// Create config
|
|
document.getElementById('create-config-btn').addEventListener('click', async function() {
|
|
const title = document.getElementById('config-title').value.trim();
|
|
const description = document.getElementById('config-description').value.trim();
|
|
const siteCheckboxes = document.querySelectorAll('.site-checkbox:checked');
|
|
const siteIds = Array.from(siteCheckboxes).map(cb => parseInt(cb.value));
|
|
|
|
if (!title) {
|
|
showError('create-config-error', 'Title is required');
|
|
return;
|
|
}
|
|
|
|
if (siteIds.length === 0) {
|
|
showError('create-config-error', 'At least one site must be selected');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/configs', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ title, description: description || null, site_ids: siteIds })
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.message || 'Failed to create config');
|
|
}
|
|
|
|
// Close modal and reload
|
|
bootstrap.Modal.getInstance(document.getElementById('createConfigModal')).hide();
|
|
document.getElementById('create-config-form').reset();
|
|
renderSitesCheckboxes(); // Reset checkboxes
|
|
await loadConfigs();
|
|
} catch (error) {
|
|
showError('create-config-error', error.message);
|
|
}
|
|
});
|
|
|
|
// View config
|
|
async function viewConfig(id) {
|
|
try {
|
|
const response = await fetch(`/api/configs/${id}`);
|
|
if (!response.ok) throw new Error('Failed to load config');
|
|
|
|
const config = await response.json();
|
|
|
|
let html = `
|
|
<div class="mb-3">
|
|
<strong>Title:</strong> ${escapeHtml(config.title)}
|
|
</div>
|
|
<div class="mb-3">
|
|
<strong>Description:</strong> ${config.description ? escapeHtml(config.description) : '<span class="text-muted">None</span>'}
|
|
</div>
|
|
<div class="mb-3">
|
|
<strong>Sites (${config.site_count}):</strong>
|
|
<ul class="mt-2">
|
|
${config.sites.map(site => `
|
|
<li>${escapeHtml(site.name)} <small class="text-muted">(${site.cidr_count} CIDR${site.cidr_count !== 1 ? 's' : ''})</small></li>
|
|
`).join('')}
|
|
</ul>
|
|
</div>
|
|
<div class="mb-3">
|
|
<strong>Created:</strong> ${formatDate(config.created_at)}
|
|
</div>
|
|
<div class="mb-3">
|
|
<strong>Last Updated:</strong> ${formatDate(config.updated_at)}
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('view-config-content').innerHTML = html;
|
|
new bootstrap.Modal(document.getElementById('viewConfigModal')).show();
|
|
} catch (error) {
|
|
alert('Error loading config: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Edit config
|
|
async function editConfig(id) {
|
|
try {
|
|
const response = await fetch(`/api/configs/${id}`);
|
|
if (!response.ok) throw new Error('Failed to load config');
|
|
|
|
const config = await response.json();
|
|
|
|
document.getElementById('edit-config-id').value = config.id;
|
|
document.getElementById('edit-config-title').value = config.title;
|
|
document.getElementById('edit-config-description').value = config.description || '';
|
|
|
|
const selectedIds = config.sites.map(s => s.id);
|
|
renderSitesCheckboxes(selectedIds, true); // true = isEditMode
|
|
|
|
new bootstrap.Modal(document.getElementById('editConfigModal')).show();
|
|
} catch (error) {
|
|
alert('Error loading config: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Save edited config
|
|
document.getElementById('edit-config-btn').addEventListener('click', async function() {
|
|
const id = document.getElementById('edit-config-id').value;
|
|
const title = document.getElementById('edit-config-title').value.trim();
|
|
const description = document.getElementById('edit-config-description').value.trim();
|
|
const siteCheckboxes = document.querySelectorAll('.edit-site-checkbox:checked');
|
|
const siteIds = Array.from(siteCheckboxes).map(cb => parseInt(cb.value));
|
|
|
|
if (!title) {
|
|
showError('edit-config-error', 'Title is required');
|
|
return;
|
|
}
|
|
|
|
if (siteIds.length === 0) {
|
|
showError('edit-config-error', 'At least one site must be selected');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/configs/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ title, description: description || null, site_ids: siteIds })
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.message || 'Failed to update config');
|
|
}
|
|
|
|
// Close modal and reload
|
|
bootstrap.Modal.getInstance(document.getElementById('editConfigModal')).hide();
|
|
await loadConfigs();
|
|
} catch (error) {
|
|
showError('edit-config-error', error.message);
|
|
}
|
|
});
|
|
|
|
// Delete config
|
|
function deleteConfig(id, name) {
|
|
document.getElementById('delete-config-id').value = id;
|
|
document.getElementById('delete-config-name').textContent = name;
|
|
new bootstrap.Modal(document.getElementById('deleteConfigModal')).show();
|
|
}
|
|
|
|
// Confirm delete
|
|
document.getElementById('confirm-delete-btn').addEventListener('click', async function() {
|
|
const id = document.getElementById('delete-config-id').value;
|
|
|
|
try {
|
|
const response = await fetch(`/api/configs/${id}`, { method: 'DELETE' });
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.message || 'Failed to delete config');
|
|
}
|
|
|
|
// Close modal and reload
|
|
bootstrap.Modal.getInstance(document.getElementById('deleteConfigModal')).hide();
|
|
await loadConfigs();
|
|
} catch (error) {
|
|
alert('Error deleting config: ' + error.message);
|
|
}
|
|
});
|
|
|
|
// Utility functions
|
|
function showError(elementId, message) {
|
|
const errorEl = document.getElementById(elementId);
|
|
const messageEl = document.getElementById(elementId + '-message');
|
|
messageEl.textContent = message;
|
|
errorEl.style.display = 'block';
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '-';
|
|
const date = new Date(dateStr);
|
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
|
}
|
|
</script>
|
|
{% endblock %}
|