The sites page previously showed total IP count which included duplicates across multiple sites, leading to inflated numbers. Now displays unique IP count as the primary metric with duplicate count shown when present. - Add get_global_ip_stats() method to SiteService for unique/duplicate counts - Update /api/sites?all=true endpoint to include IP statistics - Update sites.html to display unique IPs with optional duplicate indicator - Update API documentation with new response fields 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1188 lines
50 KiB
HTML
1188 lines
50 KiB
HTML
{% 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>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="unique-ips">-</div>
|
|
<div class="stat-label">Unique IPs</div>
|
|
<div class="stat-sublabel" id="duplicate-ips-label" style="display: none; font-size: 0.75rem; color: #fbbf24;">
|
|
(<span id="duplicate-ips">0</span> duplicates)
|
|
</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">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>IPs</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 organize your IP addresses</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">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="bi bi-plus-circle me-2"></i>Create New Site
|
|
</h5>
|
|
<button type="button" class="btn-close" 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">Site Name <span class="text-danger">*</span></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">Description</label>
|
|
<textarea class="form-control" id="site-description" rows="3"
|
|
placeholder="Optional description of this site"></textarea>
|
|
</div>
|
|
<div class="alert alert-info">
|
|
<i class="bi bi-info-circle me-1"></i>After creating the site, you'll be able to add IP addresses using CIDRs, individual IPs, or bulk import.
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<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>
|
|
|
|
<div class="d-flex justify-content-between align-items-center mt-3 mb-2">
|
|
<h6 style="color: #94a3b8; margin: 0;">IP Addresses (<span id="ip-count">0</span>):</h6>
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown">
|
|
<i class="bi bi-plus-circle"></i> Add IPs
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-dark">
|
|
<li><a class="dropdown-item" href="#" onclick="showAddIpMethod('cidr'); return false;"><i class="bi bi-diagram-3"></i> From CIDR</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="showAddIpMethod('individual'); return false;"><i class="bi bi- hdd-network"></i> Individual IP</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="showAddIpMethod('bulk'); return false;"><i class="bi bi-file-earmark-text"></i> Bulk Import</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add from CIDR Form -->
|
|
<div id="add-cidr-form" style="display: none; margin-bottom: 15px; padding: 15px; background-color: #0f172a; border: 1px solid #334155; border-radius: 5px;">
|
|
<h6 style="color: #60a5fa;"><i class="bi bi-diagram-3"></i> Add IPs from CIDR</h6>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">CIDR Range *</label>
|
|
<input type="text" class="form-control form-control-sm" id="bulk-cidr" placeholder="e.g., 10.0.0.0/24">
|
|
<small style="color: #64748b;">Max /24 (256 IPs)</small>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected Ping</label>
|
|
<select class="form-select form-select-sm" id="bulk-cidr-ping">
|
|
<option value="null">Not Set</option>
|
|
<option value="true">Yes</option>
|
|
<option value="false" selected>No</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-2">
|
|
<div class="col-md-6">
|
|
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected TCP Ports</label>
|
|
<input type="text" class="form-control form-control-sm" id="bulk-cidr-tcp-ports" placeholder="e.g., 22,80,443">
|
|
<small style="color: #64748b;">Comma-separated</small>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected UDP Ports</label>
|
|
<input type="text" class="form-control form-control-sm" id="bulk-cidr-udp-ports" placeholder="e.g., 53,123">
|
|
<small style="color: #64748b;">Comma-separated</small>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<button class="btn btn-sm btn-primary" onclick="addIpsFromCidr()">
|
|
<i class="bi bi-check-circle"></i> Add IPs
|
|
</button>
|
|
<button class="btn btn-sm btn-secondary" onclick="hideAllAddForms()">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Individual IP Form -->
|
|
<div id="add-individual-form" style="display: none; margin-bottom: 15px; padding: 15px; background-color: #0f172a; border: 1px solid #334155; border-radius: 5px;">
|
|
<h6 style="color: #60a5fa;"><i class="bi bi-hdd-network"></i> Add Individual IP</h6>
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">IP Address *</label>
|
|
<input type="text" class="form-control form-control-sm" id="individual-ip" placeholder="e.g., 192.168.1.100">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected Ping</label>
|
|
<select class="form-select form-select-sm" id="individual-ping">
|
|
<option value="null">Not Set</option>
|
|
<option value="true">Yes</option>
|
|
<option value="false" selected>No</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="row mt-2">
|
|
<div class="col-md-6">
|
|
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected TCP Ports</label>
|
|
<input type="text" class="form-control form-control-sm" id="individual-tcp-ports" placeholder="e.g., 22,80,443">
|
|
<small style="color: #64748b;">Comma-separated</small>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected UDP Ports</label>
|
|
<input type="text" class="form-control form-control-sm" id="individual-udp-ports" placeholder="e.g., 53,123">
|
|
<small style="color: #64748b;">Comma-separated</small>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<button class="btn btn-sm btn-primary" onclick="addIndividualIp()">
|
|
<i class="bi bi-check-circle"></i> Add IP
|
|
</button>
|
|
<button class="btn btn-sm btn-secondary" onclick="hideAllAddForms()">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bulk Import Form -->
|
|
<div id="add-bulk-form" style="display: none; margin-bottom: 15px; padding: 15px; background-color: #0f172a; border: 1px solid #334155; border-radius: 5px;">
|
|
<h6 style="color: #60a5fa;"><i class="bi bi-file-earmark-text"></i> Bulk Import IPs</h6>
|
|
<div class="mb-2">
|
|
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">IP Addresses *</label>
|
|
<textarea class="form-control form-control-sm" id="bulk-ips" rows="5" placeholder="Paste IPs here (one per line, or comma/space separated)"></textarea>
|
|
<small style="color: #64748b;">Supports: one per line, comma-separated, or space-separated</small>
|
|
</div>
|
|
<div class="row mt-2">
|
|
<div class="col-md-4">
|
|
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected Ping</label>
|
|
<select class="form-select form-select-sm" id="bulk-ping">
|
|
<option value="null">Not Set</option>
|
|
<option value="true">Yes</option>
|
|
<option value="false" selected>No</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected TCP Ports</label>
|
|
<input type="text" class="form-control form-control-sm" id="bulk-tcp-ports" placeholder="e.g., 22,80,443">
|
|
<small style="color: #64748b;">Comma-separated</small>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label" style="color: #e2e8f0; font-size: 0.875rem;">Expected UDP Ports</label>
|
|
<input type="text" class="form-control form-control-sm" id="bulk-udp-ports" placeholder="e.g., 53,123">
|
|
<small style="color: #64748b;">Comma-separated</small>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<button class="btn btn-sm btn-primary" onclick="addIpsFromBulk()">
|
|
<i class="bi bi-check-circle"></i> Import IPs
|
|
</button>
|
|
<button class="btn btn-sm btn-secondary" onclick="hideAllAddForms()">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- IP Table -->
|
|
<div id="view-site-ips-container">
|
|
<div id="ips-loading" style="display: none;" class="text-center py-3">
|
|
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
|
|
<span class="ms-2" style="color: #94a3b8;">Loading IPs...</span>
|
|
</div>
|
|
<div id="view-site-ips" class="table-responsive">
|
|
<!-- Will be populated by JavaScript -->
|
|
</div>
|
|
</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 manage IP addresses, use the "View" button on 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>
|
|
|
|
<!-- Edit IP Modal -->
|
|
<div class="modal fade" id="editIpModal" 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: #60a5fa;">
|
|
<i class="bi bi-pencil"></i> Edit IP Settings
|
|
</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-ip-site-id">
|
|
<input type="hidden" id="edit-ip-id">
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label" style="color: #e2e8f0;">IP Address</label>
|
|
<input type="text" class="form-control" id="edit-ip-address" readonly>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="edit-ip-ping" class="form-label" style="color: #e2e8f0;">Expected Ping</label>
|
|
<select class="form-select" id="edit-ip-ping">
|
|
<option value="null">Not Set</option>
|
|
<option value="true">Yes</option>
|
|
<option value="false">No</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="edit-ip-tcp-ports" class="form-label" style="color: #e2e8f0;">Expected TCP Ports</label>
|
|
<input type="text" class="form-control" id="edit-ip-tcp-ports" placeholder="e.g., 22,80,443">
|
|
<small style="color: #64748b;">Comma-separated port numbers</small>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="edit-ip-udp-ports" class="form-label" style="color: #e2e8f0;">Expected UDP Ports</label>
|
|
<input type="text" class="form-control" id="edit-ip-udp-ports" placeholder="e.g., 53,123">
|
|
<small style="color: #64748b;">Comma-separated port numbers</small>
|
|
</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-ip-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 currentViewingSiteId = null; // Track the site ID currently being viewed
|
|
let currentSitePage = 1;
|
|
|
|
// Helper function to clean up any stray modal backdrops
|
|
function cleanupModalBackdrops() {
|
|
// Remove any leftover backdrops
|
|
document.querySelectorAll('.modal-backdrop').forEach(backdrop => {
|
|
backdrop.remove();
|
|
});
|
|
// Remove modal-open class from body
|
|
document.body.classList.remove('modal-open');
|
|
document.body.style.removeProperty('overflow');
|
|
document.body.style.removeProperty('padding-right');
|
|
}
|
|
|
|
// Helper function to show a modal (reuses existing instance if available)
|
|
function showModal(modalId) {
|
|
const modalElement = document.getElementById(modalId);
|
|
let modal = bootstrap.Modal.getInstance(modalElement);
|
|
if (!modal) {
|
|
modal = new bootstrap.Modal(modalElement);
|
|
}
|
|
modal.show();
|
|
return modal;
|
|
}
|
|
|
|
// Format date
|
|
function formatDate(timestamp) {
|
|
if (!timestamp) return 'Unknown';
|
|
const date = new Date(timestamp);
|
|
return date.toLocaleString();
|
|
}
|
|
|
|
// Show/hide add IP form methods
|
|
function showAddIpMethod(method) {
|
|
hideAllAddForms();
|
|
if (method === 'cidr') {
|
|
document.getElementById('add-cidr-form').style.display = 'block';
|
|
} else if (method === 'individual') {
|
|
document.getElementById('add-individual-form').style.display = 'block';
|
|
} else if (method === 'bulk') {
|
|
document.getElementById('add-bulk-form').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function hideAllAddForms() {
|
|
document.getElementById('add-cidr-form').style.display = 'none';
|
|
document.getElementById('add-individual-form').style.display = 'none';
|
|
document.getElementById('add-bulk-form').style.display = 'none';
|
|
// Reset forms
|
|
document.getElementById('bulk-cidr').value = '';
|
|
document.getElementById('individual-ip').value = '';
|
|
document.getElementById('bulk-ips').value = '';
|
|
}
|
|
|
|
// 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(data.unique_ips, data.duplicate_ips);
|
|
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(uniqueIps, duplicateIps) {
|
|
const totalSites = sitesData.length;
|
|
|
|
document.getElementById('total-sites').textContent = totalSites;
|
|
document.getElementById('unique-ips').textContent = uniqueIps || 0;
|
|
|
|
// Show duplicate count if there are any
|
|
if (duplicateIps && duplicateIps > 0) {
|
|
document.getElementById('duplicate-ips').textContent = duplicateIps;
|
|
document.getElementById('duplicate-ips-label').style.display = 'block';
|
|
} else {
|
|
document.getElementById('duplicate-ips-label').style.display = 'none';
|
|
}
|
|
|
|
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 ipCount = site.ip_count || 0;
|
|
const ipBadge = ipCount > 0
|
|
? `<span class="badge bg-info">${ipCount} IP${ipCount !== 1 ? 's' : ''}</span>`
|
|
: '<span class="badge bg-secondary">No IPs</span>';
|
|
|
|
return `
|
|
<tr>
|
|
<td><strong style="color: #60a5fa;">${site.name}</strong></td>
|
|
<td style="color: #94a3b8;">${site.description || '<em>No description</em>'}</td>
|
|
<td>${ipBadge}</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;
|
|
}
|
|
|
|
const response = await fetch('/api/sites', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
name: name,
|
|
description: description || null
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.message || `HTTP ${response.status}`);
|
|
}
|
|
|
|
const newSite = await response.json();
|
|
|
|
// Hide create modal
|
|
bootstrap.Modal.getInstance(document.getElementById('createSiteModal')).hide();
|
|
|
|
// Reset form
|
|
document.getElementById('create-site-form').reset();
|
|
|
|
// Reload sites
|
|
await loadSites();
|
|
|
|
// Show success message and open view modal
|
|
showAlert('success', `Site "${name}" created successfully. Now add some IP addresses!`);
|
|
|
|
// Open the view modal to add IPs
|
|
viewSite(newSite.id);
|
|
|
|
} catch (error) {
|
|
console.error('Error creating site:', error);
|
|
showAlert('danger', `Error creating site: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// View site details
|
|
async function viewSite(siteId) {
|
|
try {
|
|
currentViewingSiteId = siteId;
|
|
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';
|
|
|
|
// Load IPs
|
|
await loadSiteIps(siteId);
|
|
|
|
// 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;
|
|
}
|
|
|
|
showModal('viewSiteModal');
|
|
|
|
} catch (error) {
|
|
console.error('Error viewing site:', error);
|
|
showAlert('danger', `Error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Load IPs for a site
|
|
async function loadSiteIps(siteId) {
|
|
try {
|
|
document.getElementById('ips-loading').style.display = 'block';
|
|
|
|
const response = await fetch(`/api/sites/${siteId}/ips?per_page=100`);
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load IPs');
|
|
}
|
|
|
|
const data = await response.json();
|
|
const ips = data.ips || [];
|
|
|
|
// Sort IPs by numeric octets
|
|
ips.sort((a, b) => {
|
|
const partsA = a.ip_address.split('.').map(Number);
|
|
const partsB = b.ip_address.split('.').map(Number);
|
|
for (let i = 0; i < 4; i++) {
|
|
if (partsA[i] !== partsB[i]) {
|
|
return partsA[i] - partsB[i];
|
|
}
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
document.getElementById('ip-count').textContent = data.total || ips.length;
|
|
|
|
// Render flat IP table
|
|
if (ips.length === 0) {
|
|
document.getElementById('view-site-ips').innerHTML = `
|
|
<div class="text-center py-4" style="color: #94a3b8;">
|
|
<i class="bi bi-hdd-network" style="font-size: 2rem;"></i>
|
|
<p class="mt-2"><em>No IPs added yet</em></p>
|
|
<p class="text-muted" style="font-size: 0.875rem;">Use the "Add IPs" button above to get started</p>
|
|
</div>
|
|
`;
|
|
} else {
|
|
const tableHtml = `
|
|
<table class="table table-sm table-hover">
|
|
<thead style="position: sticky; top: 0; background-color: #1e293b;">
|
|
<tr>
|
|
<th>IP Address</th>
|
|
<th>Ping</th>
|
|
<th>TCP Ports</th>
|
|
<th>UDP Ports</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${ips.map(ip => `
|
|
<tr>
|
|
<td><code>${ip.ip_address}</code></td>
|
|
<td>${ip.expected_ping ? '<span class="badge bg-success">Yes</span>' : '<span class="badge bg-secondary">No</span>'}</td>
|
|
<td style="font-size: 0.875rem;">${ip.expected_tcp_ports?.length > 0 ? ip.expected_tcp_ports.join(', ') : '-'}</td>
|
|
<td style="font-size: 0.875rem;">${ip.expected_udp_ports?.length > 0 ? ip.expected_udp_ports.join(', ') : '-'}</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm" role="group">
|
|
<button class="btn btn-outline-primary" onclick="editIp(${siteId}, ${ip.id}, '${ip.ip_address}', ${ip.expected_ping}, ${JSON.stringify(ip.expected_tcp_ports || [])}, ${JSON.stringify(ip.expected_udp_ports || [])})" title="Edit">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
<button class="btn btn-outline-danger" onclick="confirmDeleteIp(${siteId}, ${ip.id}, '${ip.ip_address}')" title="Delete IP">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
document.getElementById('view-site-ips').innerHTML = tableHtml;
|
|
}
|
|
|
|
document.getElementById('ips-loading').style.display = 'none';
|
|
|
|
} catch (error) {
|
|
console.error('Error loading IPs:', error);
|
|
document.getElementById('view-site-ips').innerHTML = `<p style="color: #f87171;">Error loading IPs: ${error.message}</p>`;
|
|
document.getElementById('ips-loading').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Add IPs from CIDR
|
|
async function addIpsFromCidr() {
|
|
try {
|
|
const cidr = document.getElementById('bulk-cidr').value.trim();
|
|
if (!cidr) {
|
|
showAlert('warning', 'CIDR is required');
|
|
return;
|
|
}
|
|
|
|
const pingValue = document.getElementById('bulk-cidr-ping').value;
|
|
const expectedPing = pingValue === 'null' ? null : (pingValue === 'true');
|
|
const expectedTcpPorts = parsePortList(document.getElementById('bulk-cidr-tcp-ports').value);
|
|
const expectedUdpPorts = parsePortList(document.getElementById('bulk-cidr-udp-ports').value);
|
|
|
|
const response = await fetch(`/api/sites/${currentViewingSiteId}/ips/bulk`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
source_type: 'cidr',
|
|
cidr: cidr,
|
|
expected_ping: expectedPing,
|
|
expected_tcp_ports: expectedTcpPorts,
|
|
expected_udp_ports: expectedUdpPorts
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.message || `HTTP ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
hideAllAddForms();
|
|
await loadSiteIps(currentViewingSiteId);
|
|
await loadSites(); // Refresh stats
|
|
|
|
showAlert('success', `Added ${result.ip_count} IPs from CIDR ${cidr}${result.ips_skipped.length > 0 ? ` (${result.ips_skipped.length} duplicates skipped)` : ''}`);
|
|
|
|
} catch (error) {
|
|
console.error('Error adding IPs from CIDR:', error);
|
|
showAlert('danger', `Error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Add individual IP
|
|
async function addIndividualIp() {
|
|
try {
|
|
const ipAddress = document.getElementById('individual-ip').value.trim();
|
|
if (!ipAddress) {
|
|
showAlert('warning', 'IP address is required');
|
|
return;
|
|
}
|
|
|
|
const pingValue = document.getElementById('individual-ping').value;
|
|
const expectedPing = pingValue === 'null' ? null : (pingValue === 'true');
|
|
const expectedTcpPorts = parsePortList(document.getElementById('individual-tcp-ports').value);
|
|
const expectedUdpPorts = parsePortList(document.getElementById('individual-udp-ports').value);
|
|
|
|
const response = await fetch(`/api/sites/${currentViewingSiteId}/ips`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
ip_address: ipAddress,
|
|
expected_ping: expectedPing,
|
|
expected_tcp_ports: expectedTcpPorts,
|
|
expected_udp_ports: expectedUdpPorts
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.message || `HTTP ${response.status}`);
|
|
}
|
|
|
|
hideAllAddForms();
|
|
await loadSiteIps(currentViewingSiteId);
|
|
await loadSites(); // Refresh stats
|
|
|
|
showAlert('success', `IP ${ipAddress} added successfully`);
|
|
|
|
} catch (error) {
|
|
console.error('Error adding IP:', error);
|
|
showAlert('danger', `Error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Add IPs from bulk import
|
|
async function addIpsFromBulk() {
|
|
try {
|
|
const bulkText = document.getElementById('bulk-ips').value.trim();
|
|
if (!bulkText) {
|
|
showAlert('warning', 'IP list is required');
|
|
return;
|
|
}
|
|
|
|
// Parse IPs from text (supports newlines, commas, spaces)
|
|
const ipList = bulkText.split(/[\n,\s]+/).map(ip => ip.trim()).filter(ip => ip);
|
|
|
|
if (ipList.length === 0) {
|
|
showAlert('warning', 'No valid IPs found');
|
|
return;
|
|
}
|
|
|
|
const pingValue = document.getElementById('bulk-ping').value;
|
|
const expectedPing = pingValue === 'null' ? null : (pingValue === 'true');
|
|
const expectedTcpPorts = parsePortList(document.getElementById('bulk-tcp-ports').value);
|
|
const expectedUdpPorts = parsePortList(document.getElementById('bulk-udp-ports').value);
|
|
|
|
const response = await fetch(`/api/sites/${currentViewingSiteId}/ips/bulk`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
source_type: 'list',
|
|
ips: ipList,
|
|
expected_ping: expectedPing,
|
|
expected_tcp_ports: expectedTcpPorts,
|
|
expected_udp_ports: expectedUdpPorts
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.message || `HTTP ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
hideAllAddForms();
|
|
await loadSiteIps(currentViewingSiteId);
|
|
await loadSites(); // Refresh stats
|
|
|
|
let message = `Added ${result.ip_count} IPs`;
|
|
if (result.ips_skipped.length > 0) message += ` (${result.ips_skipped.length} duplicates skipped)`;
|
|
if (result.errors.length > 0) message += ` (${result.errors.length} errors)`;
|
|
|
|
showAlert(result.errors.length > 0 ? 'warning' : 'success', message);
|
|
|
|
} catch (error) {
|
|
console.error('Error adding IPs from bulk:', error);
|
|
showAlert('danger', `Error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Confirm delete IP
|
|
function confirmDeleteIp(siteId, ipId, ipAddress) {
|
|
if (confirm(`Are you sure you want to delete IP ${ipAddress}?`)) {
|
|
deleteIp(siteId, ipId);
|
|
}
|
|
}
|
|
|
|
// Delete IP
|
|
async function deleteIp(siteId, ipId) {
|
|
try {
|
|
const response = await fetch(`/api/sites/${siteId}/ips/${ipId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.message || `HTTP ${response.status}`);
|
|
}
|
|
|
|
await loadSiteIps(siteId);
|
|
await loadSites(); // Refresh stats
|
|
|
|
showAlert('success', 'IP deleted successfully');
|
|
|
|
} catch (error) {
|
|
console.error('Error deleting IP:', error);
|
|
showAlert('danger', `Error deleting IP: ${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
|
|
showModal('editSiteModal');
|
|
} 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);
|
|
}
|
|
|
|
showModal('deleteModal');
|
|
}
|
|
|
|
// 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}`);
|
|
}
|
|
}
|
|
|
|
// Edit IP settings
|
|
function editIp(siteId, ipId, ipAddress, expectedPing, expectedTcpPorts, expectedUdpPorts) {
|
|
// Populate modal
|
|
document.getElementById('edit-ip-site-id').value = siteId;
|
|
document.getElementById('edit-ip-id').value = ipId;
|
|
document.getElementById('edit-ip-address').value = ipAddress;
|
|
|
|
// Set ping value
|
|
const pingValue = expectedPing === null ? 'null' : (expectedPing ? 'true' : 'false');
|
|
document.getElementById('edit-ip-ping').value = pingValue;
|
|
|
|
// Set ports
|
|
document.getElementById('edit-ip-tcp-ports').value = expectedTcpPorts && expectedTcpPorts.length > 0 ? expectedTcpPorts.join(',') : '';
|
|
document.getElementById('edit-ip-udp-ports').value = expectedUdpPorts && expectedUdpPorts.length > 0 ? expectedUdpPorts.join(',') : '';
|
|
|
|
// Show modal
|
|
showModal('editIpModal');
|
|
}
|
|
|
|
// Save IP settings
|
|
async function saveIp() {
|
|
try {
|
|
const siteId = document.getElementById('edit-ip-site-id').value;
|
|
const ipId = document.getElementById('edit-ip-id').value;
|
|
const ipAddress = document.getElementById('edit-ip-address').value;
|
|
|
|
const pingValue = document.getElementById('edit-ip-ping').value;
|
|
const expectedPing = pingValue === 'null' ? null : (pingValue === 'true');
|
|
|
|
const expectedTcpPorts = parsePortList(document.getElementById('edit-ip-tcp-ports').value);
|
|
const expectedUdpPorts = parsePortList(document.getElementById('edit-ip-udp-ports').value);
|
|
|
|
const response = await fetch(`/api/sites/${siteId}/ips/${ipId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
expected_ping: expectedPing,
|
|
expected_tcp_ports: expectedTcpPorts,
|
|
expected_udp_ports: expectedUdpPorts
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.message || `HTTP ${response.status}`);
|
|
}
|
|
|
|
// Hide modal
|
|
bootstrap.Modal.getInstance(document.getElementById('editIpModal')).hide();
|
|
|
|
// Reload the view site modal
|
|
await viewSite(siteId);
|
|
|
|
showAlert('success', `IP ${ipAddress} updated successfully`);
|
|
|
|
} catch (error) {
|
|
console.error('Error saving IP:', error);
|
|
showAlert('danger', `Error saving IP: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Show alert
|
|
function showAlert(type, message) {
|
|
const container = document.getElementById('notification-container');
|
|
const notification = document.createElement('div');
|
|
notification.className = `alert alert-${type} alert-dismissible fade show mb-2`;
|
|
|
|
notification.innerHTML = `
|
|
${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
container.appendChild(notification);
|
|
|
|
// Auto-dismiss after 5 seconds
|
|
setTimeout(() => {
|
|
notification.remove();
|
|
}, 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);
|
|
|
|
// Setup save IP button
|
|
document.getElementById('save-ip-btn').addEventListener('click', saveIp);
|
|
|
|
// Add cleanup listeners to all modals
|
|
['createSiteModal', 'viewSiteModal', 'editSiteModal', 'deleteModal', 'editIpModal'].forEach(modalId => {
|
|
const modalElement = document.getElementById(modalId);
|
|
modalElement.addEventListener('hidden.bs.modal', function() {
|
|
// Clean up any stray backdrops when modal is fully hidden
|
|
setTimeout(cleanupModalBackdrops, 100);
|
|
});
|
|
});
|
|
|
|
// Load sites on page load
|
|
document.addEventListener('DOMContentLoaded', loadSites);
|
|
</script>
|
|
|
|
{% endblock %}
|