378 lines
14 KiB
HTML
378 lines
14 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Configuration Files - SneakyScanner{% endblock %}
|
|
|
|
{% block extra_styles %}
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/config-manager.css') }}">
|
|
{% 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;">Configuration Files</h1>
|
|
<div>
|
|
<a href="{{ url_for('main.upload_config') }}" class="btn btn-primary">
|
|
<i class="bi bi-plus-circle"></i> Create New Config
|
|
</a>
|
|
</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="configs-in-use">-</div>
|
|
<div class="stat-label">In Use by Schedules</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="total-size">-</div>
|
|
<div class="stat-label">Total Size</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>Filename</th>
|
|
<th>Title</th>
|
|
<th>Created</th>
|
|
<th>Size</th>
|
|
<th>Used By</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-file-earmark-text" style="font-size: 3rem; color: #64748b;"></i>
|
|
<h5 class="mt-3 text-muted">No configuration files</h5>
|
|
<p class="text-muted">Create your first config to define scan targets</p>
|
|
<a href="{{ url_for('main.upload_config') }}" class="btn btn-primary mt-2">
|
|
<i class="bi bi-plus-circle"></i> Create Config
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</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 config file:</p>
|
|
<p style="color: #60a5fa; font-weight: bold;" id="delete-config-name"></p>
|
|
<p style="color: #fbbf24;" id="delete-warning-schedules" style="display: none;">
|
|
<i class="bi bi-exclamation-circle"></i>
|
|
This config is used by schedules and cannot be deleted.
|
|
</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>
|
|
|
|
<!-- View Config Modal -->
|
|
<div class="modal fade" id="viewModal" 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-file-earmark-code"></i> Config File Details
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<h6 style="color: #94a3b8;">Filename: <span id="view-filename" style="color: #e2e8f0;"></span></h6>
|
|
<h6 class="mt-3" style="color: #94a3b8;">Content:</h6>
|
|
<pre style="background-color: #0f172a; border: 1px solid #334155; padding: 15px; border-radius: 5px; max-height: 400px; overflow-y: auto;"><code id="view-content" style="color: #e2e8f0;"></code></pre>
|
|
</div>
|
|
<div class="modal-footer" style="border-top: 1px solid #334155;">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
<a id="download-link" href="#" class="btn btn-primary">
|
|
<i class="bi bi-download"></i> Download
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
// Global variables
|
|
let configsData = [];
|
|
let selectedConfigForDeletion = null;
|
|
|
|
// Format file size
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
// Format date
|
|
function formatDate(timestamp) {
|
|
if (!timestamp) return 'Unknown';
|
|
const date = new Date(timestamp);
|
|
return date.toLocaleString();
|
|
}
|
|
|
|
// Load configs from API
|
|
async function loadConfigs() {
|
|
try {
|
|
document.getElementById('configs-loading').style.display = 'block';
|
|
document.getElementById('configs-error').style.display = 'none';
|
|
document.getElementById('configs-content').style.display = 'none';
|
|
|
|
const response = await fetch('/api/configs');
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
configsData = data.configs || [];
|
|
|
|
updateStats();
|
|
renderConfigs(configsData);
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Update summary stats
|
|
function updateStats() {
|
|
const totalConfigs = configsData.length;
|
|
const configsInUse = configsData.filter(c => c.used_by_schedules && c.used_by_schedules.length > 0).length;
|
|
const totalSize = configsData.reduce((sum, c) => sum + (c.size_bytes || 0), 0);
|
|
|
|
document.getElementById('total-configs').textContent = totalConfigs;
|
|
document.getElementById('configs-in-use').textContent = configsInUse;
|
|
document.getElementById('total-size').textContent = formatFileSize(totalSize);
|
|
}
|
|
|
|
// Render configs table
|
|
function renderConfigs(configs) {
|
|
const tbody = document.getElementById('configs-tbody');
|
|
const emptyState = document.getElementById('empty-state');
|
|
|
|
if (configs.length === 0) {
|
|
tbody.innerHTML = '';
|
|
emptyState.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
emptyState.style.display = 'none';
|
|
|
|
tbody.innerHTML = configs.map(config => {
|
|
const usedByBadge = config.used_by_schedules && config.used_by_schedules.length > 0
|
|
? `<span class="badge bg-info" title="${config.used_by_schedules.join(', ')}">${config.used_by_schedules.length} schedule(s)</span>`
|
|
: '<span class="badge bg-secondary">Not used</span>';
|
|
|
|
return `
|
|
<tr>
|
|
<td><code>${config.filename}</code></td>
|
|
<td>${config.title || config.filename}</td>
|
|
<td>${formatDate(config.created_at)}</td>
|
|
<td>${formatFileSize(config.size_bytes || 0)}</td>
|
|
<td>${usedByBadge}</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm" role="group">
|
|
<button class="btn btn-outline-primary" onclick="viewConfig('${config.filename}')" title="View">
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
<a href="/configs/edit/${config.filename}" class="btn btn-outline-info" title="Edit">
|
|
<i class="bi bi-pencil"></i>
|
|
</a>
|
|
<a href="/api/configs/${config.filename}/download" class="btn btn-outline-success" title="Download">
|
|
<i class="bi bi-download"></i>
|
|
</a>
|
|
<button class="btn btn-outline-danger" onclick="confirmDelete('${config.filename}', ${config.used_by_schedules.length > 0})" title="Delete">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// View config details
|
|
async function viewConfig(filename) {
|
|
try {
|
|
const response = await fetch(`/api/configs/${filename}`);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to load config: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
document.getElementById('view-filename').textContent = data.filename;
|
|
document.getElementById('view-content').textContent = data.content;
|
|
document.getElementById('download-link').href = `/api/configs/${filename}/download`;
|
|
|
|
new bootstrap.Modal(document.getElementById('viewModal')).show();
|
|
|
|
} catch (error) {
|
|
console.error('Error viewing config:', error);
|
|
alert(`Error: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Confirm delete
|
|
function confirmDelete(filename, isInUse) {
|
|
selectedConfigForDeletion = filename;
|
|
document.getElementById('delete-config-name').textContent = filename;
|
|
|
|
const warningDiv = document.getElementById('delete-warning-schedules');
|
|
const deleteBtn = document.getElementById('confirm-delete-btn');
|
|
|
|
if (isInUse) {
|
|
warningDiv.style.display = 'block';
|
|
deleteBtn.disabled = true;
|
|
deleteBtn.classList.add('disabled');
|
|
} else {
|
|
warningDiv.style.display = 'none';
|
|
deleteBtn.disabled = false;
|
|
deleteBtn.classList.remove('disabled');
|
|
}
|
|
|
|
new bootstrap.Modal(document.getElementById('deleteModal')).show();
|
|
}
|
|
|
|
// Delete config
|
|
async function deleteConfig() {
|
|
if (!selectedConfigForDeletion) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/configs/${selectedConfigForDeletion}`, {
|
|
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 configs
|
|
await loadConfigs();
|
|
|
|
// Show success message
|
|
showAlert('success', `Config "${selectedConfigForDeletion}" deleted successfully`);
|
|
|
|
} catch (error) {
|
|
console.error('Error deleting config:', error);
|
|
showAlert('danger', `Error deleting config: ${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) {
|
|
renderConfigs(configsData);
|
|
return;
|
|
}
|
|
|
|
const filtered = configsData.filter(config =>
|
|
config.filename.toLowerCase().includes(searchTerm) ||
|
|
(config.title && config.title.toLowerCase().includes(searchTerm))
|
|
);
|
|
|
|
renderConfigs(filtered);
|
|
});
|
|
|
|
// Setup delete button
|
|
document.getElementById('confirm-delete-btn').addEventListener('click', deleteConfig);
|
|
|
|
// Load configs on page load
|
|
document.addEventListener('DOMContentLoaded', loadConfigs);
|
|
</script>
|
|
{% endblock %}
|