first commit
This commit is contained in:
496
static/css/style.css
Normal file
496
static/css/style.css
Normal 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
75
static/js/api.js
Normal 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
375
static/js/app.js
Normal 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 <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
276
static/js/cache.js
Normal 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
197
static/js/progress.js
Normal 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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user