first commit

This commit is contained in:
Mateusz Gruszczyński
2026-02-17 09:04:09 +01:00
commit c0afc1554d
32 changed files with 7217 additions and 0 deletions

496
static/css/style.css Normal file
View File

@@ -0,0 +1,496 @@
body {
background-color: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
padding-bottom: 2rem;
}
html {
scroll-behavior: smooth;
}
.card {
border: 1px solid #e0e0e0;
border-radius: 0.5rem;
margin-bottom: 1.5rem;
animation: fadeIn 0.4s ease-out;
}
.card-header {
background-color: #ffffff;
border-bottom: 2px solid #e0e0e0;
padding: 1.25rem 1.5rem;
}
.card-header h4 {
color: #212529;
font-weight: 600;
}
.card-body {
background-color: #ffffff;
}
.shadow-sm {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
}
#countryList {
max-height: 600px;
overflow-y: auto;
padding: 0.75rem;
background-color: #fafafa;
border: 1px solid #e0e0e0;
border-radius: 0.375rem;
margin-bottom: 0.75rem;
}
#countryList::-webkit-scrollbar {
width: 8px;
}
#countryList::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
#countryList::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
#countryList::-webkit-scrollbar-thumb:hover {
background: #555;
}
.form-check-compact {
padding: 0.25rem 0.5rem;
margin-bottom: 0.15rem;
transition: background-color 0.1s;
border-radius: 0.25rem;
display: flex;
align-items: center;
gap: 0.4rem;
}
.form-check-compact:hover {
background-color: #e9ecef;
}
.form-check-compact .form-check-input {
width: 1rem;
height: 1rem;
margin: 0;
cursor: pointer;
flex-shrink: 0;
position: relative;
}
.form-check-compact .form-check-label {
cursor: pointer;
user-select: none;
font-size: 0.75rem;
padding: 0;
margin: 0;
font-family: 'Courier New', monospace;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4;
flex: 1;
min-width: 0;
}
#countryList .col-lg-2,
#countryList .col-md-3,
#countryList .col-sm-4,
#countryList .col-6 {
padding: 0.15rem;
}
#countryList .form-check {
padding-left: 0;
min-height: auto;
}
.form-label {
color: #495057;
font-size: 1rem;
margin-bottom: 0.75rem;
}
.form-select,
.form-control {
border-radius: 0.375rem;
border: 1px solid #ced4da;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-select:focus,
.form-control:focus {
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.form-select-lg {
padding: 0.75rem 1rem;
font-size: 1.05rem;
}
.form-switch {
padding-left: 0;
min-height: auto;
}
.form-switch .form-check-input {
width: 3rem;
height: 1.5rem;
margin-left: 0;
margin-right: 1rem;
float: left;
cursor: pointer;
}
.form-switch .form-check-label {
display: inline-block;
padding-left: 0;
padding-top: 0.125rem;
}
.aggregate-card {
background-color: #f8f9fa;
border: 1px solid #e0e0e0;
border-radius: 0.375rem;
padding: 1rem;
}
.aggregate-card .form-check {
padding: 0;
margin-bottom: 0;
}
.btn {
border-radius: 0.375rem;
font-weight: 500;
transition: all 0.2s ease;
}
.btn-primary {
background-color: #0d6efd;
border-color: #0d6efd;
}
.btn-primary:hover {
background-color: #0b5ed7;
border-color: #0a58ca;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(13, 110, 253, 0.3);
}
.btn-lg {
padding: 0.875rem 1.5rem;
font-size: 1.125rem;
}
.btn-outline-primary {
color: #0d6efd;
border-color: #0d6efd;
}
.btn-outline-primary:hover {
background-color: #0d6efd;
border-color: #0d6efd;
color: white;
}
.btn-outline-secondary {
color: #6c757d;
border-color: #6c757d;
}
.btn-outline-secondary:hover {
background-color: #6c757d;
border-color: #6c757d;
color: white;
}
.btn:disabled {
cursor: not-allowed;
opacity: 0.65;
}
.alert {
border-radius: 0.5rem;
border: none;
padding: 1rem 1.25rem;
}
.alert i {
font-size: 1.1rem;
vertical-align: middle;
}
.alert-info {
background-color: #d1ecf1;
color: #0c5460;
}
.alert-success {
background-color: #d4edda;
color: #155724;
}
.alert-warning {
background-color: #fff3cd;
color: #856404;
}
.alert-danger {
background-color: #f8d7da;
color: #721c24;
}
.progress {
border-radius: 0.5rem;
background-color: #e9ecef;
overflow: hidden;
}
.progress-bar {
font-size: 0.95rem;
font-weight: 500;
}
.navbar {
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
padding: 1rem 0;
}
.navbar-brand {
font-weight: 600;
font-size: 1.25rem;
color: #212529;
}
.navbar-brand img {
max-height: 30px;
}
.footer {
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
padding: 1.5rem 0;
margin-top: 3rem;
}
.footer a {
color: #0d6efd;
text-decoration: none;
transition: color 0.2s;
}
.footer a:hover {
color: #0a58ca;
text-decoration: underline;
}
.modal-xl {
max-width: 90%;
}
#previewContent {
background-color: #282c34 !important;
color: #abb2bf !important;
padding: 1.5rem;
border-radius: 0.375rem;
font-family: 'Courier New', Consolas, Monaco, monospace;
font-size: 0.875rem;
line-height: 1.5;
max-height: 70vh;
overflow: auto;
white-space: pre;
word-wrap: normal;
display: block !important;
}
.modal-body {
padding: 1.5rem;
}
.modal-body pre {
margin-bottom: 0;
background-color: transparent;
}
.modal-body pre code {
display: block;
background-color: #282c34;
color: #abb2bf;
}
#previewContent::-webkit-scrollbar {
width: 10px;
height: 10px;
}
#previewContent::-webkit-scrollbar-track {
background: #21252b;
}
#previewContent::-webkit-scrollbar-thumb {
background: #4b5263;
border-radius: 5px;
}
#previewContent::-webkit-scrollbar-thumb:hover {
background: #5c6370;
}
.api-header-get {
background-color: #e7f3ff;
border-left: 4px solid #0dcaf0;
cursor: pointer;
transition: background-color 0.2s;
}
.api-header-get:hover {
background-color: #d1ecf1;
}
.api-header-post {
background-color: #d4edda;
border-left: 4px solid #198754;
cursor: pointer;
transition: background-color 0.2s;
}
.api-header-post:hover {
background-color: #c3e6cb;
}
.api-path {
font-size: 1rem;
font-weight: 600;
color: #212529;
}
.api-endpoint pre {
background-color: #282c34;
color: #abb2bf;
padding: 1rem;
border-radius: 0.375rem;
overflow-x: auto;
margin-bottom: 0;
}
.api-endpoint pre code {
background-color: transparent;
color: inherit;
padding: 0;
font-size: 0.9rem;
line-height: 1.6;
}
.api-endpoint .text-success {
color: #98c379 !important;
}
.api-endpoint .text-warning {
color: #e5c07b !important;
}
.api-endpoint .text-info {
color: #61afef !important;
}
.api-endpoint .text-danger {
color: #e06c75 !important;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (min-width: 1400px) {
#countryList .col-lg-2 {
flex: 0 0 12.5%;
max-width: 12.5%;
}
}
@media (max-width: 1199px) {
#countryList .col-lg-2 {
flex: 0 0 16.666%;
max-width: 16.666%;
}
}
@media (max-width: 991px) {
#countryList .col-md-3 {
flex: 0 0 20%;
max-width: 20%;
}
}
@media (max-width: 767px) {
.card-body {
padding: 1.5rem !important;
}
#countryList {
max-height: 300px;
}
#countryList .col-sm-4 {
flex: 0 0 25%;
max-width: 25%;
}
.form-check-compact .form-check-label {
font-size: 0.75rem;
}
.form-select-lg,
.btn-lg {
font-size: 1rem;
padding: 0.75rem 1.25rem;
}
.navbar-brand {
font-size: 1rem;
}
.modal-xl {
max-width: 95%;
}
}
@media (max-width: 575px) {
#countryList .col-6 {
flex: 0 0 33.333%;
max-width: 33.333%;
}
}
@media (max-width: 399px) {
#countryList .col-6 {
flex: 0 0 50%;
max-width: 50%;
}
}
#variantDescription {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-5px); }
to { opacity: 1; transform: translateY(0); }
}

75
static/js/api.js Normal file
View File

@@ -0,0 +1,75 @@
const baseUrl = window.location.origin;
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('baseUrl').textContent = baseUrl;
document.querySelectorAll('[id^="curlUrl"]').forEach(element => {
element.textContent = baseUrl;
});
});
function toggleEndpoint(id) {
const element = document.getElementById(id);
const bsCollapse = new bootstrap.Collapse(element, {
toggle: true
});
}
function tryEndpoint(endpoint, method = 'GET') {
const url = baseUrl + '/api/' + endpoint;
const responseId = 'response-' + endpoint.replace(/\//g, '-');
const responseDiv = document.getElementById(responseId);
const responseBody = document.getElementById(responseId + '-body');
responseDiv.style.display = 'block';
responseBody.textContent = 'Loading...';
const options = {
method: method,
headers: {
'Content-Type': 'application/json'
}
};
fetch(url, options)
.then(response => response.json())
.then(data => {
responseBody.textContent = JSON.stringify(data, null, 2);
})
.catch(error => {
responseBody.textContent = 'Error: ' + error.message;
});
}
function tryInvalidateCountry() {
const countryInput = document.getElementById('invalidateCountry');
const country = countryInput.value.trim().toUpperCase();
if (!country || country.length !== 2) {
alert('Please enter a valid 2-letter country code (e.g., CN, RU, US)');
return;
}
const url = baseUrl + '/api/cache/invalidate/' + country;
const responseDiv = document.getElementById('response-cache-invalidate');
const responseBody = document.getElementById('response-cache-invalidate-body');
responseDiv.style.display = 'block';
responseBody.textContent = 'Loading...';
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
responseBody.textContent = JSON.stringify(data, null, 2);
if (data.success) {
countryInput.value = '';
}
})
.catch(error => {
responseBody.textContent = 'Error: ' + error.message;
});
}

375
static/js/app.js Normal file
View File

@@ -0,0 +1,375 @@
const BASE_URL = window.location.origin;
const APP_VARIANTS = {
'raw-cidr': [
{
value: 'txt',
text: 'Plain Text (.txt)',
description: 'Simple list of CIDR blocks, one per line'
},
{
value: 'csv',
text: 'CSV Format (.csv)',
description: 'Structured CSV with country codes and networks'
}
],
nginx: [
{
value: 'deny',
text: 'Deny Directives',
description: 'Simple and fast. Works everywhere. Recommended for large lists.',
recommended: true
},
{
value: 'geo',
text: 'Geo Module',
description: 'Fast with native CIDR support. Requires http_geo_module compiled in nginx.'
},
{
value: 'map',
text: 'Map Module (regex)',
description: 'Slow with 10k+ rules. Uses regex patterns. Not recommended for production.',
warning: true
}
],
apache: [
{
value: '24',
text: 'Apache 2.4 (Require)',
description: 'Modern Apache 2.4+ syntax using Require directives'
},
{
value: '22',
text: 'Apache 2.2 (Allow/Deny)',
description: 'Legacy Apache 2.2 syntax with Allow/Deny directives'
}
],
haproxy: [
{
value: 'acl',
text: 'ACL Rules',
description: 'Native HAProxy ACL rules for frontend/backend blocking'
},
{
value: 'lua',
text: 'Lua Script',
description: 'Lua-based blocking script for advanced HAProxy setups'
}
]
};
document.addEventListener('DOMContentLoaded', function() {
updateVariants();
checkDatabaseStatus();
});
function updateVariants() {
const appType = document.getElementById('appType').value;
const variantSelect = document.getElementById('appVariant');
const variantSection = document.getElementById('variantSection');
const variants = APP_VARIANTS[appType] || [];
variantSelect.innerHTML = '';
variantSection.style.display = 'block';
variants.forEach(variant => {
const option = document.createElement('option');
option.value = variant.value;
option.textContent = variant.text;
option.dataset.description = variant.description || '';
option.dataset.warning = variant.warning || false;
option.dataset.recommended = variant.recommended || false;
variantSelect.appendChild(option);
});
updateVariantDescription();
}
function updateVariantDescription() {
const variantSelect = document.getElementById('appVariant');
const descriptionDiv = document.getElementById('variantDescription');
if (!descriptionDiv) return;
const selectedOption = variantSelect.options[variantSelect.selectedIndex];
if (selectedOption && selectedOption.dataset.description) {
const isWarning = selectedOption.dataset.warning === 'true';
const isRecommended = selectedOption.dataset.recommended === 'true';
let alertClass = 'alert-info';
let borderClass = 'border-info';
let icon = 'fa-info-circle';
if (isRecommended) {
alertClass = 'alert-success';
borderClass = 'border-success';
icon = 'fa-check-circle';
} else if (isWarning) {
alertClass = 'alert-warning';
borderClass = 'border-warning';
icon = 'fa-exclamation-triangle';
}
descriptionDiv.innerHTML = `
<div class="alert ${alertClass} border-start border-4 ${borderClass} mb-0 py-2">
<small>
<i class="fas ${icon} me-2"></i>
${selectedOption.dataset.description}
</small>
</div>
`;
descriptionDiv.style.display = 'block';
} else {
descriptionDiv.style.display = 'none';
}
}
function checkDatabaseStatus() {
fetch(BASE_URL + '/api/database/status')
.then(response => response.json())
.then(data => {
const statusDiv = document.getElementById('dbStatus');
if (data.success) {
if (data.exists && !data.needs_update) {
statusDiv.className = 'alert alert-success mb-0';
statusDiv.innerHTML = '<i class="fas fa-check-circle me-2"></i>Database ready (Last update: ' + formatDate(data.last_update) + ')';
} else if (data.needs_update) {
statusDiv.className = 'alert alert-warning mb-0';
statusDiv.innerHTML = '<i class="fas fa-exclamation-triangle me-2"></i>Database needs update <button class="btn btn-sm btn-warning ms-2" onclick="updateDatabase()">Update Now</button>';
} else {
statusDiv.className = 'alert alert-info mb-0';
statusDiv.innerHTML = '<i class="fas fa-download me-2"></i>Downloading database...';
}
}
})
.catch(error => {
console.error('Error:', error);
});
}
function updateDatabase() {
const statusDiv = document.getElementById('dbStatus');
statusDiv.className = 'alert alert-info mb-0';
statusDiv.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Updating database...';
fetch(BASE_URL + '/api/database/update', { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
statusDiv.className = 'alert alert-success mb-0';
statusDiv.innerHTML = '<i class="fas fa-check-circle me-2"></i>Database updated successfully';
setTimeout(checkDatabaseStatus, 2000);
} else {
statusDiv.className = 'alert alert-danger mb-0';
statusDiv.innerHTML = '<i class="fas fa-times-circle me-2"></i>Update failed: ' + data.error;
}
});
}
function selectAll() {
const checkboxes = document.querySelectorAll('input[name="countries"]');
checkboxes.forEach(cb => cb.checked = true);
}
function deselectAll() {
const checkboxes = document.querySelectorAll('input[name="countries"]');
checkboxes.forEach(cb => cb.checked = false);
}
function formatDate(dateString) {
if (!dateString) return 'Never';
try {
const date = new Date(dateString);
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
} catch (e) {
return dateString;
}
}
function copyToClipboard() {
const content = document.getElementById('previewContent').textContent;
navigator.clipboard.writeText(content).then(() => {
showResult('Copied to clipboard!', 'success');
}).catch(err => {
const textarea = document.createElement('textarea');
textarea.value = content;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
showResult('Copied to clipboard!', 'success');
} catch (e) {
showResult('Failed to copy to clipboard', 'danger');
}
document.body.removeChild(textarea);
});
}
function getFormData() {
const countries = Array.from(document.querySelectorAll('input[name="countries"]:checked'))
.map(input => input.value);
if (countries.length === 0) {
return null;
}
const useCacheCheckbox = document.getElementById('useCache');
return {
countries: countries,
app_type: document.getElementById('appType').value,
app_variant: document.getElementById('appVariant').value,
aggregate: document.getElementById('aggregate').checked,
use_cache: useCacheCheckbox ? useCacheCheckbox.checked : true
};
}
function showCacheBadge(fromCache, generatedAt) {
if (fromCache) {
const badge = document.createElement('div');
badge.className = 'alert alert-success alert-dismissible fade show mt-3';
badge.innerHTML = `
<i class="fas fa-bolt me-2"></i>
<strong>Lightning fast!</strong> Config loaded from Redis cache in &lt;100ms
<small class="d-block mt-1">Generated: ${new Date(generatedAt).toLocaleString()}</small>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
const container = document.querySelector('.container > .row > .col-lg-10');
container.insertBefore(badge, container.firstChild);
setTimeout(() => {
badge.classList.remove('show');
setTimeout(() => badge.remove(), 150);
}, 5000);
}
}
async function previewConfiguration() {
const formData = getFormData();
if (!formData) {
showResult('Please select at least one country to continue', 'warning');
return;
}
showProgress();
try {
const endpoint = formData.app_type === 'raw-cidr'
? '/api/generate/raw'
: '/api/generate/preview';
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
hideProgress();
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
showResult('Error: ' + (errorData.error || 'Request failed'), 'danger');
return;
}
if (formData.app_type === 'raw-cidr') {
const text = await response.text();
const fromCache = response.headers.get('X-From-Cache') === 'true';
const generatedAt = response.headers.get('X-Generated-At');
const contentDisposition = response.headers.get('Content-Disposition');
let filename = 'blocklist.txt';
if (contentDisposition) {
const matches = /filename="?(.+)"?/.exec(contentDisposition);
if (matches) filename = matches[1];
}
document.getElementById('previewContent').textContent = text;
const cacheIndicator = document.getElementById('cacheIndicator');
if (fromCache) {
cacheIndicator.innerHTML = '<span class="badge bg-success ms-2"><i class="fas fa-bolt"></i> From Cache</span>';
showCacheBadge(true, generatedAt);
} else {
cacheIndicator.innerHTML = '<span class="badge bg-info ms-2"><i class="fas fa-sync"></i> Fresh</span>';
}
window.lastGeneratedConfig = text;
window.lastGeneratedFilename = filename;
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
modal.show();
} else {
const result = await response.json();
if (result.success) {
document.getElementById('previewContent').textContent = result.config;
const cacheIndicator = document.getElementById('cacheIndicator');
if (result.from_cache) {
cacheIndicator.innerHTML = '<span class="badge bg-success ms-2"><i class="fas fa-bolt"></i> From Cache</span>';
showCacheBadge(true, result.generated_at);
} else {
cacheIndicator.innerHTML = '<span class="badge bg-info ms-2"><i class="fas fa-sync"></i> Fresh</span>';
}
if (result.stats) {
const statsText = `${result.stats.countries} countries, ${result.stats.total_networks.toLocaleString()} networks`;
document.getElementById('previewStats').textContent = statsText;
}
window.lastGeneratedConfig = result.config;
window.currentStats = result.stats;
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
modal.show();
} else {
showResult(result.error || 'An error occurred while generating the preview', 'danger');
}
}
} catch (error) {
hideProgress();
showResult('Network error: ' + error.message, 'danger');
console.error('Preview error:', error);
}
}
async function downloadFromPreview() {
const formData = getFormData();
if (!formData) {
showResult('Please select at least one country', 'warning');
return;
}
const modal = bootstrap.Modal.getInstance(document.getElementById('previewModal'));
if (modal) {
modal.hide();
}
await downloadConfiguration(formData);
}

276
static/js/cache.js Normal file
View File

@@ -0,0 +1,276 @@
async function loadCacheStats() {
const container = document.getElementById('cacheStatsContent');
if (!container) return;
container.innerHTML = `
<div class="text-center py-3">
<div class="spinner-border spinner-border-sm text-primary" role="status"></div>
<span class="ms-2">Loading statistics...</span>
</div>
`;
try {
const [cacheResponse, sqliteResponse] = await Promise.all([
fetch('/api/cache/status'),
fetch('/api/database/sqlite/status')
]);
const cacheData = await cacheResponse.json();
const sqliteData = await sqliteResponse.json();
if (!cacheData.success) {
container.innerHTML = `<div class="alert alert-warning mb-0">Redis cache unavailable: ${cacheData.error || 'Unknown error'}</div>`;
return;
}
const stats = cacheData.stats || {};
const health = cacheData.health || {};
let html = `
<h6 class="mb-3"><i class="fas fa-bolt text-warning me-2"></i>Redis Cache</h6>
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="text-center p-3 bg-light rounded">
<h4 class="mb-1">${stats.country_keys || 0}</h4>
<small class="text-muted">Country Keys</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center p-3 bg-light rounded">
<h4 class="mb-1">${stats.config_keys || 0}</h4>
<small class="text-muted">Config Keys</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center p-3 bg-light rounded">
<h4 class="mb-1">${stats.total_size_mb || 0} MB</h4>
<small class="text-muted">Cache Size</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center p-3 bg-light rounded">
<h4 class="mb-1">${health.memory_used_mb || 0} MB</h4>
<small class="text-muted">Memory Used</small>
</div>
</div>
</div>
`;
if (sqliteData.success && sqliteData.exists) {
const modifiedDate = new Date(sqliteData.modified).toLocaleString();
html += `
<h6 class="mb-3"><i class="fas fa-database text-primary me-2"></i>SQLite Cache Database</h6>
<div class="row g-3 mb-3">
<div class="col-md-3">
<div class="text-center p-3 bg-light rounded">
<h4 class="mb-1">${sqliteData.total_countries || 0}</h4>
<small class="text-muted">Countries</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center p-3 bg-light rounded">
<h4 class="mb-1">${(sqliteData.total_networks || 0).toLocaleString()}</h4>
<small class="text-muted">Total Networks</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center p-3 bg-light rounded">
<h4 class="mb-1">${sqliteData.file_size_mb || 0} MB</h4>
<small class="text-muted">Database Size</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center p-3 bg-light rounded">
<small class="text-muted d-block mb-1">Last Modified</small>
<small><strong>${modifiedDate}</strong></small>
</div>
</div>
</div>
`;
if (sqliteData.top_countries && sqliteData.top_countries.length > 0) {
html += `
<div class="alert alert-info mb-0">
<strong><i class="fas fa-star me-1"></i>Top countries:</strong>
${sqliteData.top_countries.map(c =>
`<span class="badge bg-secondary ms-2">${c.code}: ${c.networks.toLocaleString()}</span>`
).join('')}
</div>
`;
}
} else {
html += `
<div class="alert alert-warning mb-0">
<i class="fas fa-exclamation-triangle me-2"></i>
SQLite cache database not available
</div>
`;
}
container.innerHTML = html;
} catch (error) {
console.error('Error loading cache stats:', error);
container.innerHTML = `
<div class="alert alert-danger mb-0">
<i class="fas fa-times-circle me-2"></i>
Failed to load statistics: ${error.message}
</div>
`;
}
}
async function flushCache() {
const confirmed = await showConfirmModal(
'Flush Redis Cache',
'Are you sure you want to flush ALL Redis cache?<br><br>' +
'<strong>This will delete:</strong><br>' +
'• All cached country data<br>' +
'• All cached configurations<br>' +
'• Force regeneration for future requests<br><br>' +
'<span class="text-danger">This action cannot be undone!</span>'
);
if (!confirmed) return;
try {
showFlushingIndicator();
const response = await fetch('/api/cache/flush', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
});
const data = await response.json();
hideFlushingIndicator();
if (data.success) {
showToast('success', 'Cache Flushed', 'All Redis cache has been cleared successfully!');
loadCacheStats();
} else {
showToast('danger', 'Error', 'Failed to flush cache: ' + (data.error || 'Unknown error'));
}
} catch (error) {
hideFlushingIndicator();
showToast('danger', 'Error', 'Network error: ' + error.message);
}
}
function showConfirmModal(title, message) {
return new Promise((resolve) => {
const modalId = 'confirmModal_' + Date.now();
const modalHtml = `
<div class="modal fade" id="${modalId}" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-warning text-dark">
<h5 class="modal-title">
<i class="fas fa-exclamation-triangle me-2"></i>${title}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
${message}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-1"></i>Cancel
</button>
<button type="button" class="btn btn-danger" id="confirmBtn">
<i class="fas fa-trash me-1"></i>Flush Cache
</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modalEl = document.getElementById(modalId);
const modal = new bootstrap.Modal(modalEl);
modalEl.querySelector('#confirmBtn').addEventListener('click', () => {
modal.hide();
resolve(true);
});
modalEl.addEventListener('hidden.bs.modal', () => {
modalEl.remove();
resolve(false);
});
modal.show();
});
}
function showFlushingIndicator() {
const indicator = document.createElement('div');
indicator.id = 'flushingIndicator';
indicator.className = 'position-fixed top-50 start-50 translate-middle';
indicator.style.zIndex = '9999';
indicator.innerHTML = `
<div class="card shadow-lg">
<div class="card-body text-center p-4">
<div class="spinner-border text-warning mb-3" role="status" style="width: 3rem; height: 3rem;">
<span class="visually-hidden">Flushing...</span>
</div>
<h5>Flushing Cache...</h5>
<p class="text-muted mb-0">Please wait</p>
</div>
</div>
`;
document.body.appendChild(indicator);
}
function hideFlushingIndicator() {
const indicator = document.getElementById('flushingIndicator');
if (indicator) {
indicator.remove();
}
}
function showToast(type, title, message) {
const toastId = 'toast_' + Date.now();
const bgClass = type === 'success' ? 'bg-success' : type === 'danger' ? 'bg-danger' : 'bg-warning';
const toastHtml = `
<div class="position-fixed top-0 end-0 p-3" style="z-index: 9999">
<div id="${toastId}" class="toast ${bgClass} text-white" role="alert">
<div class="toast-header ${bgClass} text-white">
<i class="fas fa-${type === 'success' ? 'check-circle' : 'exclamation-circle'} me-2"></i>
<strong class="me-auto">${title}</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body">
${message}
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', toastHtml);
const toastEl = document.getElementById(toastId);
const toast = new bootstrap.Toast(toastEl, { delay: 5000 });
toast.show();
toastEl.addEventListener('hidden.bs.toast', () => {
toastEl.parentElement.remove();
});
}
function showAlert(type, message) {
showToast(type, type === 'success' ? 'Success' : 'Error', message);
}
document.addEventListener('DOMContentLoaded', function() {
const statsPanel = document.getElementById('cacheStatsPanel');
if (statsPanel) {
statsPanel.addEventListener('shown.bs.collapse', function() {
loadCacheStats();
});
}
});

197
static/js/progress.js Normal file
View File

@@ -0,0 +1,197 @@
let progressInterval = null;
function startProgressPolling() {
if (progressInterval) {
clearInterval(progressInterval);
}
progressInterval = setInterval(async () => {
try {
const response = await fetch('/api/progress');
const data = await response.json();
if (data.active) {
updateProgressUI(data.message, data.progress, data.total);
} else {
stopProgressPolling();
}
} catch (error) {
console.error('Progress polling error:', error);
}
}, 500);
}
function stopProgressPolling() {
if (progressInterval) {
clearInterval(progressInterval);
progressInterval = null;
}
}
function updateProgressUI(message, progress, total) {
const progressSection = document.getElementById('progressSection');
const progressBar = progressSection.querySelector('.progress-bar');
const progressMessage = document.getElementById('progressMessage');
const progressPercentage = document.getElementById('progressPercentage');
const percentage = total > 0 ? Math.round((progress / total) * 100) : 0;
progressBar.style.width = percentage + '%';
if (progressPercentage) {
progressPercentage.textContent = percentage + '%';
}
progressBar.setAttribute('aria-valuenow', percentage);
if (progressMessage) {
progressMessage.textContent = message;
}
}
function showProgress() {
const progressSection = document.getElementById('progressSection');
const progressBar = progressSection.querySelector('.progress-bar');
const progressMessage = document.getElementById('progressMessage');
const progressPercentage = document.getElementById('progressPercentage');
progressBar.style.width = '0%';
if (progressPercentage) {
progressPercentage.textContent = '0%';
}
if (progressMessage) {
progressMessage.textContent = 'Initializing...';
}
document.getElementById('resultSection').style.display = 'none';
progressSection.style.display = 'block';
document.getElementById('generateBtn').disabled = true;
startProgressPolling();
}
function hideProgress() {
const progressSection = document.getElementById('progressSection');
progressSection.style.display = 'none';
document.getElementById('generateBtn').disabled = false;
stopProgressPolling();
}
function showResult(message, type = 'danger') {
const resultSection = document.getElementById('resultSection');
const resultMessage = document.getElementById('resultMessage');
const alertDiv = resultSection.querySelector('.alert');
const iconMap = {
'success': 'check-circle',
'danger': 'exclamation-circle',
'warning': 'exclamation-triangle',
'info': 'info-circle'
};
const icon = iconMap[type] || 'info-circle';
alertDiv.className = `alert alert-${type}`;
resultMessage.innerHTML = `<i class="fas fa-${icon} me-2"></i>${message}`;
resultSection.style.display = 'block';
if (type === 'success') {
setTimeout(() => {
resultSection.style.display = 'none';
}, 5000);
}
}
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('generateForm');
if (form) {
form.addEventListener('submit', async function(e) {
e.preventDefault();
const formData = getFormData();
if (!formData) {
showResult('Please select at least one country to continue', 'warning');
return;
}
await downloadConfiguration(formData);
});
}
});
async function downloadConfiguration(formData) {
showProgress();
try {
const endpoint = formData.app_type === 'raw-cidr'
? '/api/generate/raw'
: '/api/generate';
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
hideProgress();
if (response.ok) {
const fromCache = response.headers.get('X-From-Cache') === 'true';
const generatedAt = response.headers.get('X-Generated-At');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const contentDisposition = response.headers.get('Content-Disposition');
let filename = 'geoblock_config.conf';
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
if (fromCache) {
showResult(`<i class="fas fa-bolt"></i> <strong>Lightning fast!</strong> Downloaded from cache: ${filename}`, 'success');
} else {
showResult(`Configuration downloaded successfully: ${filename}`, 'success');
}
} else {
const error = await response.json();
showResult(error.error || 'An error occurred during download', 'danger');
}
} catch (error) {
hideProgress();
showResult('Network error: ' + error.message, 'danger');
}
}
function getFormData() {
const countries = Array.from(document.querySelectorAll('input[name="countries"]:checked'))
.map(input => input.value);
if (countries.length === 0) {
return null;
}
const useCacheCheckbox = document.getElementById('useCache');
return {
countries: countries,
app_type: document.getElementById('appType').value,
app_variant: document.getElementById('appVariant').value,
aggregate: document.getElementById('aggregate').checked,
use_cache: useCacheCheckbox ? useCacheCheckbox.checked : true
};
}