stage 1 of doing new cidrs/ site setup
This commit is contained in:
775
app/web/templates/sites.html
Normal file
775
app/web/templates/sites.html
Normal file
@@ -0,0 +1,775 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Sites - 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;">Site Management</h1>
|
||||
<div>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createSiteModal">
|
||||
<i class="bi bi-plus-circle"></i> Create New Site
|
||||
</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-sites">-</div>
|
||||
<div class="stat-label">Total Sites</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="total-cidrs">-</div>
|
||||
<div class="stat-label">Total CIDRs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="sites-in-use">-</div>
|
||||
<div class="stat-label">Used in Scans</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sites 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 Sites</h5>
|
||||
<input type="text" id="search-input" class="form-control" style="max-width: 300px;"
|
||||
placeholder="Search sites...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="sites-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 sites...</p>
|
||||
</div>
|
||||
<div id="sites-error" style="display: none;" class="alert alert-danger">
|
||||
<strong>Error:</strong> <span id="error-message"></span>
|
||||
</div>
|
||||
<div id="sites-content" style="display: none;">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Site Name</th>
|
||||
<th>Description</th>
|
||||
<th>CIDRs</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sites-tbody">
|
||||
<!-- Populated by JavaScript -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="empty-state" style="display: none;" class="text-center py-5">
|
||||
<i class="bi bi-globe" style="font-size: 3rem; color: #64748b;"></i>
|
||||
<h5 class="mt-3 text-muted">No sites defined</h5>
|
||||
<p class="text-muted">Create your first site to group CIDR ranges</p>
|
||||
<button class="btn btn-primary mt-2" data-bs-toggle="modal" data-bs-target="#createSiteModal">
|
||||
<i class="bi bi-plus-circle"></i> Create Site
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Site Modal -->
|
||||
<div class="modal fade" id="createSiteModal" 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 Site
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="create-site-form">
|
||||
<div class="mb-3">
|
||||
<label for="site-name" class="form-label" style="color: #e2e8f0;">Site Name *</label>
|
||||
<input type="text" class="form-control" id="site-name" required
|
||||
placeholder="e.g., Production Web Servers">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="site-description" class="form-label" style="color: #e2e8f0;">Description</label>
|
||||
<textarea class="form-control" id="site-description" rows="2"
|
||||
placeholder="Optional description"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" style="color: #e2e8f0;">CIDR Ranges *</label>
|
||||
<div id="cidrs-container">
|
||||
<!-- CIDR inputs will be added here -->
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addCidrInput()">
|
||||
<i class="bi bi-plus"></i> Add CIDR
|
||||
</button>
|
||||
</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" onclick="createSite()">
|
||||
<i class="bi bi-check-circle"></i> Create Site
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View Site Modal -->
|
||||
<div class="modal fade" id="viewSiteModal" 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-globe"></i> <span id="view-site-name"></span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h6 style="color: #94a3b8;">Description:</h6>
|
||||
<p id="view-site-description" style="color: #e2e8f0;"></p>
|
||||
|
||||
<h6 class="mt-3" style="color: #94a3b8;">CIDR Ranges:</h6>
|
||||
<div id="view-site-cidrs" class="table-responsive">
|
||||
<!-- Will be populated -->
|
||||
</div>
|
||||
|
||||
<h6 class="mt-3" style="color: #94a3b8;">Usage:</h6>
|
||||
<div id="view-site-usage">
|
||||
<p style="color: #94a3b8;"><i class="bi bi-hourglass"></i> Loading usage...</p>
|
||||
</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>
|
||||
|
||||
<!-- Edit Site Modal -->
|
||||
<div class="modal fade" id="editSiteModal" 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 Site
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="edit-site-id">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="edit-site-name" class="form-label">Site Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="edit-site-name" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="edit-site-description" class="form-label">Description</label>
|
||||
<textarea class="form-control" id="edit-site-description" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> To edit CIDRs and IP ranges, please delete and recreate the site.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-danger" id="edit-site-error" style="display: none;">
|
||||
<span id="edit-site-error-message"></span>
|
||||
</div>
|
||||
</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="save-edit-site-btn">
|
||||
<i class="bi bi-check-circle"></i> Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteModal" 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: #f87171;">
|
||||
<i class="bi bi-exclamation-triangle"></i> Confirm Deletion
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p style="color: #e2e8f0;">Are you sure you want to delete the site:</p>
|
||||
<p style="color: #60a5fa; font-weight: bold;" id="delete-site-name"></p>
|
||||
<p style="color: #fbbf24;" id="delete-warning" style="display: none;">
|
||||
<i class="bi bi-exclamation-circle"></i>
|
||||
<span id="delete-warning-message"></span>
|
||||
</p>
|
||||
</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 variables
|
||||
let sitesData = [];
|
||||
let selectedSiteForDeletion = null;
|
||||
let cidrInputCounter = 0;
|
||||
|
||||
// Format date
|
||||
function formatDate(timestamp) {
|
||||
if (!timestamp) return 'Unknown';
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
// Add CIDR input field
|
||||
function addCidrInput(cidr = '', expectedPing = false, expectedTcpPorts = [], expectedUdpPorts = []) {
|
||||
const container = document.getElementById('cidrs-container');
|
||||
const id = cidrInputCounter++;
|
||||
|
||||
const cidrHtml = `
|
||||
<div class="cidr-input-group mb-2 p-3" style="background-color: #0f172a; border: 1px solid #334155; border-radius: 5px;" id="cidr-${id}">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" style="color: #94a3b8; font-size: 0.875rem;">CIDR *</label>
|
||||
<input type="text" class="form-control form-control-sm cidr-value" placeholder="e.g., 10.0.0.0/24" value="${cidr}" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" style="color: #94a3b8; font-size: 0.875rem;">Expect Ping</label>
|
||||
<select class="form-select form-select-sm cidr-ping">
|
||||
<option value="">Default (No)</option>
|
||||
<option value="true" ${expectedPing ? 'selected' : ''}>Yes</option>
|
||||
<option value="false" ${expectedPing === false ? 'selected' : ''}>No</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" style="color: #94a3b8; font-size: 0.875rem;">Actions</label>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger w-100" onclick="removeCidrInput(${id})">
|
||||
<i class="bi bi-trash"></i> Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" style="color: #94a3b8; font-size: 0.875rem;">Expected TCP Ports</label>
|
||||
<input type="text" class="form-control form-control-sm cidr-tcp-ports" placeholder="e.g., 22,80,443" value="${expectedTcpPorts.join(',')}">
|
||||
<small style="color: #64748b;">Comma-separated port numbers</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" style="color: #94a3b8; font-size: 0.875rem;">Expected UDP Ports</label>
|
||||
<input type="text" class="form-control form-control-sm cidr-udp-ports" placeholder="e.g., 53,123" value="${expectedUdpPorts.join(',')}">
|
||||
<small style="color: #64748b;">Comma-separated port numbers</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', cidrHtml);
|
||||
}
|
||||
|
||||
// Remove CIDR input field
|
||||
function removeCidrInput(id) {
|
||||
const element = document.getElementById(`cidr-${id}`);
|
||||
if (element) {
|
||||
element.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Parse port list from comma-separated string
|
||||
function parsePortList(portString) {
|
||||
if (!portString || !portString.trim()) return [];
|
||||
return portString.split(',')
|
||||
.map(p => parseInt(p.trim()))
|
||||
.filter(p => !isNaN(p) && p > 0 && p <= 65535);
|
||||
}
|
||||
|
||||
// Load sites from API
|
||||
async function loadSites() {
|
||||
try {
|
||||
document.getElementById('sites-loading').style.display = 'block';
|
||||
document.getElementById('sites-error').style.display = 'none';
|
||||
document.getElementById('sites-content').style.display = 'none';
|
||||
|
||||
const response = await fetch('/api/sites?all=true');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
sitesData = data.sites || [];
|
||||
|
||||
updateStats();
|
||||
renderSites(sitesData);
|
||||
|
||||
document.getElementById('sites-loading').style.display = 'none';
|
||||
document.getElementById('sites-content').style.display = 'block';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading sites:', error);
|
||||
document.getElementById('sites-loading').style.display = 'none';
|
||||
document.getElementById('sites-error').style.display = 'block';
|
||||
document.getElementById('error-message').textContent = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Update summary stats
|
||||
function updateStats() {
|
||||
const totalSites = sitesData.length;
|
||||
const totalCidrs = sitesData.reduce((sum, site) => sum + (site.cidrs?.length || 0), 0);
|
||||
|
||||
document.getElementById('total-sites').textContent = totalSites;
|
||||
document.getElementById('total-cidrs').textContent = totalCidrs;
|
||||
document.getElementById('sites-in-use').textContent = '-'; // Will be updated async
|
||||
|
||||
// Count sites in use (async)
|
||||
let sitesInUse = 0;
|
||||
sitesData.forEach(async (site) => {
|
||||
try {
|
||||
const response = await fetch(`/api/sites/${site.id}/usage`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.count > 0) {
|
||||
sitesInUse++;
|
||||
document.getElementById('sites-in-use').textContent = sitesInUse;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error checking site usage:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Render sites table
|
||||
function renderSites(sites) {
|
||||
const tbody = document.getElementById('sites-tbody');
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
|
||||
if (sites.length === 0) {
|
||||
tbody.innerHTML = '';
|
||||
emptyState.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
emptyState.style.display = 'none';
|
||||
|
||||
tbody.innerHTML = sites.map(site => {
|
||||
const cidrCount = site.cidrs?.length || 0;
|
||||
const cidrBadge = cidrCount > 0
|
||||
? `<span class="badge bg-info">${cidrCount} CIDR${cidrCount !== 1 ? 's' : ''}</span>`
|
||||
: '<span class="badge bg-secondary">No CIDRs</span>';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td><strong style="color: #60a5fa;">${site.name}</strong></td>
|
||||
<td style="color: #94a3b8;">${site.description || '<em>No description</em>'}</td>
|
||||
<td>${cidrBadge}</td>
|
||||
<td>${formatDate(site.created_at)}</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-outline-primary" onclick="viewSite(${site.id})" title="View">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-warning" onclick="editSite(${site.id})" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger" onclick="confirmDelete(${site.id}, '${site.name}')" title="Delete">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Create new site
|
||||
async function createSite() {
|
||||
try {
|
||||
const name = document.getElementById('site-name').value.trim();
|
||||
const description = document.getElementById('site-description').value.trim();
|
||||
|
||||
if (!name) {
|
||||
showAlert('warning', 'Site name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect CIDRs
|
||||
const cidrGroups = document.querySelectorAll('.cidr-input-group');
|
||||
const cidrs = [];
|
||||
|
||||
cidrGroups.forEach(group => {
|
||||
const cidrValue = group.querySelector('.cidr-value').value.trim();
|
||||
if (!cidrValue) return;
|
||||
|
||||
const pingValue = group.querySelector('.cidr-ping').value;
|
||||
const tcpPorts = parsePortList(group.querySelector('.cidr-tcp-ports').value);
|
||||
const udpPorts = parsePortList(group.querySelector('.cidr-udp-ports').value);
|
||||
|
||||
cidrs.push({
|
||||
cidr: cidrValue,
|
||||
expected_ping: pingValue === 'true' ? true : (pingValue === 'false' ? false : null),
|
||||
expected_tcp_ports: tcpPorts,
|
||||
expected_udp_ports: udpPorts
|
||||
});
|
||||
});
|
||||
|
||||
if (cidrs.length === 0) {
|
||||
showAlert('warning', 'At least one CIDR is required');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/sites', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
description: description || null,
|
||||
cidrs: cidrs
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
// Hide modal
|
||||
bootstrap.Modal.getInstance(document.getElementById('createSiteModal')).hide();
|
||||
|
||||
// Reset form
|
||||
document.getElementById('create-site-form').reset();
|
||||
document.getElementById('cidrs-container').innerHTML = '';
|
||||
cidrInputCounter = 0;
|
||||
|
||||
// Reload sites
|
||||
await loadSites();
|
||||
|
||||
// Show success message
|
||||
showAlert('success', `Site "${name}" created successfully`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating site:', error);
|
||||
showAlert('danger', `Error creating site: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// View site details
|
||||
async function viewSite(siteId) {
|
||||
try {
|
||||
const response = await fetch(`/api/sites/${siteId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load site: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const site = await response.json();
|
||||
|
||||
document.getElementById('view-site-name').textContent = site.name;
|
||||
document.getElementById('view-site-description').textContent = site.description || 'No description';
|
||||
|
||||
// Render CIDRs
|
||||
const cidrsHtml = site.cidrs && site.cidrs.length > 0
|
||||
? `<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>CIDR</th>
|
||||
<th>Ping</th>
|
||||
<th>TCP Ports</th>
|
||||
<th>UDP Ports</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${site.cidrs.map(cidr => `
|
||||
<tr>
|
||||
<td><code>${cidr.cidr}</code></td>
|
||||
<td>${cidr.expected_ping ? '<span class="badge bg-success">Yes</span>' : '<span class="badge bg-secondary">No</span>'}</td>
|
||||
<td>${cidr.expected_tcp_ports?.length > 0 ? cidr.expected_tcp_ports.join(', ') : '-'}</td>
|
||||
<td>${cidr.expected_udp_ports?.length > 0 ? cidr.expected_udp_ports.join(', ') : '-'}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>`
|
||||
: '<p style="color: #94a3b8;"><em>No CIDRs defined</em></p>';
|
||||
|
||||
document.getElementById('view-site-cidrs').innerHTML = cidrsHtml;
|
||||
|
||||
// Load usage
|
||||
document.getElementById('view-site-usage').innerHTML = '<p style="color: #94a3b8;"><i class="bi bi-hourglass"></i> Loading usage...</p>';
|
||||
|
||||
const usageResponse = await fetch(`/api/sites/${siteId}/usage`);
|
||||
if (usageResponse.ok) {
|
||||
const usage = await usageResponse.json();
|
||||
|
||||
const usageHtml = usage.count > 0
|
||||
? `<p style="color: #e2e8f0;">Used in <strong>${usage.count}</strong> scan(s)</p>
|
||||
<ul style="color: #94a3b8;">${usage.scans.map(scan =>
|
||||
`<li>${scan.title} - ${new Date(scan.timestamp).toLocaleString()} (${scan.status})</li>`
|
||||
).join('')}</ul>`
|
||||
: '<p style="color: #94a3b8;"><em>Not used in any scans</em></p>';
|
||||
|
||||
document.getElementById('view-site-usage').innerHTML = usageHtml;
|
||||
}
|
||||
|
||||
new bootstrap.Modal(document.getElementById('viewSiteModal')).show();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error viewing site:', error);
|
||||
showAlert('danger', `Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Edit site
|
||||
async function editSite(siteId) {
|
||||
try {
|
||||
const response = await fetch(`/api/sites/${siteId}`);
|
||||
if (!response.ok) throw new Error('Failed to load site');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Populate form
|
||||
document.getElementById('edit-site-id').value = data.id;
|
||||
document.getElementById('edit-site-name').value = data.name;
|
||||
document.getElementById('edit-site-description').value = data.description || '';
|
||||
|
||||
// Hide error
|
||||
document.getElementById('edit-site-error').style.display = 'none';
|
||||
|
||||
// Show modal
|
||||
new bootstrap.Modal(document.getElementById('editSiteModal')).show();
|
||||
} catch (error) {
|
||||
console.error('Error loading site:', error);
|
||||
showAlert('danger', `Error loading site: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Save edited site
|
||||
document.getElementById('save-edit-site-btn').addEventListener('click', async function() {
|
||||
const siteId = document.getElementById('edit-site-id').value;
|
||||
const name = document.getElementById('edit-site-name').value.trim();
|
||||
const description = document.getElementById('edit-site-description').value.trim();
|
||||
|
||||
if (!name) {
|
||||
document.getElementById('edit-site-error-message').textContent = 'Site name is required';
|
||||
document.getElementById('edit-site-error').style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/sites/${siteId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, description: description || null })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.message || 'Failed to update site');
|
||||
}
|
||||
|
||||
// Close modal
|
||||
bootstrap.Modal.getInstance(document.getElementById('editSiteModal')).hide();
|
||||
|
||||
// Reload sites
|
||||
await loadSites();
|
||||
|
||||
showAlert('success', `Site "${name}" updated successfully`);
|
||||
} catch (error) {
|
||||
console.error('Error updating site:', error);
|
||||
document.getElementById('edit-site-error-message').textContent = error.message;
|
||||
document.getElementById('edit-site-error').style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
// Confirm delete
|
||||
async function confirmDelete(siteId, siteName) {
|
||||
selectedSiteForDeletion = siteId;
|
||||
document.getElementById('delete-site-name').textContent = siteName;
|
||||
|
||||
// Check if site is in use
|
||||
try {
|
||||
const response = await fetch(`/api/sites/${siteId}/usage`);
|
||||
if (response.ok) {
|
||||
const usage = await response.json();
|
||||
|
||||
const warningDiv = document.getElementById('delete-warning');
|
||||
const warningMsg = document.getElementById('delete-warning-message');
|
||||
const deleteBtn = document.getElementById('confirm-delete-btn');
|
||||
|
||||
if (usage.count > 0) {
|
||||
warningDiv.style.display = 'block';
|
||||
warningMsg.textContent = `This site is used in ${usage.count} scan(s) and cannot be deleted.`;
|
||||
deleteBtn.disabled = true;
|
||||
deleteBtn.classList.add('disabled');
|
||||
} else {
|
||||
warningDiv.style.display = 'none';
|
||||
deleteBtn.disabled = false;
|
||||
deleteBtn.classList.remove('disabled');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error checking site usage:', e);
|
||||
}
|
||||
|
||||
new bootstrap.Modal(document.getElementById('deleteModal')).show();
|
||||
}
|
||||
|
||||
// Delete site
|
||||
async function deleteSite() {
|
||||
if (!selectedSiteForDeletion) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/sites/${selectedSiteForDeletion}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
// Hide modal
|
||||
bootstrap.Modal.getInstance(document.getElementById('deleteModal')).hide();
|
||||
|
||||
// Reload sites
|
||||
await loadSites();
|
||||
|
||||
// Show success message
|
||||
showAlert('success', `Site deleted successfully`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting site:', error);
|
||||
showAlert('danger', `Error deleting site: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Show alert
|
||||
function showAlert(type, message) {
|
||||
const alertHtml = `
|
||||
<div class="alert alert-${type} alert-dismissible fade show mt-3" role="alert">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const container = document.querySelector('.container-fluid');
|
||||
container.insertAdjacentHTML('afterbegin', alertHtml);
|
||||
|
||||
// Auto-dismiss after 5 seconds
|
||||
setTimeout(() => {
|
||||
const alert = container.querySelector('.alert');
|
||||
if (alert) {
|
||||
bootstrap.Alert.getInstance(alert)?.close();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Search filter
|
||||
document.getElementById('search-input').addEventListener('input', function(e) {
|
||||
const searchTerm = e.target.value.toLowerCase();
|
||||
|
||||
if (!searchTerm) {
|
||||
renderSites(sitesData);
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = sitesData.filter(site =>
|
||||
site.name.toLowerCase().includes(searchTerm) ||
|
||||
(site.description && site.description.toLowerCase().includes(searchTerm))
|
||||
);
|
||||
|
||||
renderSites(filtered);
|
||||
});
|
||||
|
||||
// Setup delete button
|
||||
document.getElementById('confirm-delete-btn').addEventListener('click', deleteSite);
|
||||
|
||||
// Initialize modal
|
||||
document.getElementById('createSiteModal').addEventListener('show.bs.modal', function() {
|
||||
// Reset form and add one CIDR input
|
||||
document.getElementById('create-site-form').reset();
|
||||
document.getElementById('cidrs-container').innerHTML = '';
|
||||
cidrInputCounter = 0;
|
||||
addCidrInput();
|
||||
});
|
||||
|
||||
// Load sites on page load
|
||||
document.addEventListener('DOMContentLoaded', loadSites);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
|
||||
border: 1px solid #475569;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #334155;
|
||||
border-bottom: 1px solid #475569;
|
||||
}
|
||||
|
||||
.table {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
color: #94a3b8;
|
||||
border-bottom: 1px solid #475569;
|
||||
}
|
||||
|
||||
.table tbody tr {
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background-color: #334155;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user