|
|
@@ -3235,6 +3235,25 @@ async function loadGeoIP() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function getGeoUsageMap() {
|
|
|
+ const usage = {};
|
|
|
+ const checkRule = (r, type) => {
|
|
|
+ ['src_geoip', 'dst_geoip'].forEach(field => {
|
|
|
+ let val = (r && r[field]) || '';
|
|
|
+ let code = val.replace(/^!/, '').replace(/^geo:/, '').replace(/^geo_/, '').toUpperCase().trim();
|
|
|
+ if (code) {
|
|
|
+ if (!usage[code]) usage[code] = [];
|
|
|
+ const label = `${type} #${r.priority !== undefined ? r.priority : (r.id || '')}`;
|
|
|
+ if (!usage[code].includes(label)) usage[code].push(label);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+ (state.filterRules || []).forEach(r => checkRule(r, 'Filter'));
|
|
|
+ (state.natRules || []).forEach(r => checkRule(r, 'NAT'));
|
|
|
+ (state.mangleRules || []).forEach(r => checkRule(r, 'Mangle'));
|
|
|
+ return usage;
|
|
|
+}
|
|
|
+
|
|
|
function updateGeoHeader() {
|
|
|
const s = state.geoStatus || {};
|
|
|
const statusBadge = document.getElementById('geo-status-badge');
|
|
|
@@ -3250,8 +3269,11 @@ function updateGeoHeader() {
|
|
|
intervalLabel.innerText = `Every ${s.interval_hours || 24}h`;
|
|
|
}
|
|
|
|
|
|
+ const usageMap = getGeoUsageMap();
|
|
|
+ const inUseCount = Object.keys(usageMap).length;
|
|
|
+
|
|
|
const activeCountEl = document.getElementById('geo-stat-active-countries');
|
|
|
- if (activeCountEl) activeCountEl.innerText = (s.enabled_count || 0).toLocaleString();
|
|
|
+ if (activeCountEl) activeCountEl.innerText = inUseCount.toLocaleString();
|
|
|
|
|
|
const totalSubnetsEl = document.getElementById('geo-stat-total-subnets');
|
|
|
if (totalSubnetsEl) totalSubnetsEl.innerText = (s.total_subnets || 0).toLocaleString();
|
|
|
@@ -3277,7 +3299,7 @@ function updateGeoHeader() {
|
|
|
}
|
|
|
|
|
|
const badge = document.getElementById('fw-badge-geo');
|
|
|
- if (badge) badge.innerText = s.enabled_count || 0;
|
|
|
+ if (badge) badge.innerText = inUseCount || (state.geoCountries || []).filter(c => c.subnet_count > 0).length || 0;
|
|
|
}
|
|
|
|
|
|
function renderGeoCountries() {
|
|
|
@@ -3288,10 +3310,14 @@ function renderGeoCountries() {
|
|
|
|
|
|
const q = (document.getElementById('geo-search-input')?.value || '').trim().toLowerCase();
|
|
|
const continent = document.getElementById('geo-continent-filter')?.value || '';
|
|
|
- const enabledOnly = document.getElementById('geo-filter-enabled-only')?.checked;
|
|
|
+ const inUseOnly = document.getElementById('geo-filter-inuse-only')?.checked;
|
|
|
+ const cachedOnly = document.getElementById('geo-filter-cached-only')?.checked;
|
|
|
+ const usageMap = getGeoUsageMap();
|
|
|
|
|
|
const countries = (state.geoCountries || []).filter(c => {
|
|
|
- if (enabledOnly && !c.enabled) return false;
|
|
|
+ const isUsed = !!usageMap[c.code];
|
|
|
+ if (inUseOnly && !isUsed) return false;
|
|
|
+ if (cachedOnly && (!c.subnet_count || c.subnet_count === 0)) return false;
|
|
|
if (continent && c.continent !== continent) return false;
|
|
|
if (q) {
|
|
|
const matchCode = c.code.toLowerCase().includes(q);
|
|
|
@@ -3309,11 +3335,22 @@ function renderGeoCountries() {
|
|
|
|
|
|
countries.forEach(c => {
|
|
|
const tr = document.createElement('tr');
|
|
|
- tr.className = `hover:bg-slate-800/40 transition text-xs ${c.enabled ? 'bg-emerald-950/10' : ''}`;
|
|
|
+ const isUsed = !!usageMap[c.code];
|
|
|
+ const rulesUsing = usageMap[c.code] || [];
|
|
|
+ tr.className = `hover:bg-slate-800/40 transition text-xs ${isUsed ? 'bg-emerald-950/20' : ''}`;
|
|
|
+
|
|
|
+ let statusHtml = '';
|
|
|
+ if (isUsed) {
|
|
|
+ statusHtml = `<span class="px-2 py-0.5 rounded text-[10px] font-mono font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/40" title="Assigned in: ${rulesUsing.join(', ')}">IN USE (${rulesUsing.length})</span>`;
|
|
|
+ } else if (c.subnet_count > 0) {
|
|
|
+ statusHtml = '<span class="px-2 py-0.5 rounded text-[10px] font-mono text-cyan-300 bg-cyan-950/40 border border-cyan-800/40">CACHED</span>';
|
|
|
+ } else {
|
|
|
+ statusHtml = '<span class="px-2 py-0.5 rounded text-[10px] font-mono text-slate-500 bg-slate-800/60">READY</span>';
|
|
|
+ }
|
|
|
|
|
|
tr.innerHTML = `
|
|
|
<td class="py-2.5 px-3 text-center">
|
|
|
- <span class="px-2 py-0.5 rounded font-mono font-bold ${c.enabled ? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/40' : 'bg-slate-800 text-cyan-300 border border-slate-700'}">
|
|
|
+ <span class="px-2 py-0.5 rounded font-mono font-bold ${isUsed ? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/40' : 'bg-slate-800 text-cyan-300 border border-slate-700'}">
|
|
|
${c.code}
|
|
|
</span>
|
|
|
</td>
|
|
|
@@ -3324,15 +3361,10 @@ function renderGeoCountries() {
|
|
|
</td>
|
|
|
<td class="py-2.5 px-3 font-mono text-purple-300">${c.ipset_name}</td>
|
|
|
<td class="py-2.5 px-3">
|
|
|
- ${c.enabled
|
|
|
- ? '<span class="px-2 py-0.5 rounded text-[10px] font-mono font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/40">ENABLED</span>'
|
|
|
- : '<span class="px-2 py-0.5 rounded text-[10px] font-mono font-bold bg-slate-800 text-slate-500">DISABLED</span>'}
|
|
|
+ ${statusHtml}
|
|
|
</td>
|
|
|
<td class="py-2.5 px-3 text-right space-x-2 font-sans whitespace-nowrap">
|
|
|
- ${c.enabled
|
|
|
- ? `<button onclick="toggleGeoCountry('${c.code}', false)" class="text-xs text-rose-400 hover:underline font-semibold">Disable</button>`
|
|
|
- : `<button onclick="toggleGeoCountry('${c.code}', true)" class="text-xs text-emerald-400 hover:underline font-semibold">Enable</button>`}
|
|
|
- <button onclick="updateGeoCountry('${c.code}')" class="text-xs text-cyan-400 hover:text-cyan-300" title="Re-download latest country IP blocks">Sync</button>
|
|
|
+ <button onclick="updateGeoCountry('${c.code}')" class="text-xs text-cyan-400 hover:text-cyan-300 font-medium" title="Re-download latest country IP blocks">Sync</button>
|
|
|
<button onclick="viewGeoSubnets('${c.code}', '${c.name.replace(/'/g, "\\'")}')" class="text-xs text-slate-400 hover:text-white" title="Preview Subnet Ranges">View</button>
|
|
|
</td>
|
|
|
`;
|
|
|
@@ -3340,6 +3372,27 @@ function renderGeoCountries() {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+function quickFilterGeo(preset) {
|
|
|
+ const searchInput = document.getElementById('geo-search-input');
|
|
|
+ const continentSel = document.getElementById('geo-continent-filter');
|
|
|
+ const inUseCheck = document.getElementById('geo-filter-inuse-only');
|
|
|
+ const cachedCheck = document.getElementById('geo-filter-cached-only');
|
|
|
+
|
|
|
+ if (searchInput) searchInput.value = '';
|
|
|
+ if (continentSel) continentSel.value = '';
|
|
|
+ if (inUseCheck) inUseCheck.checked = false;
|
|
|
+ if (cachedCheck) cachedCheck.checked = false;
|
|
|
+
|
|
|
+ if (preset === 'GR') {
|
|
|
+ if (searchInput) searchInput.value = 'Greece';
|
|
|
+ } else if (preset === 'Europe') {
|
|
|
+ if (continentSel) continentSel.value = 'Europe';
|
|
|
+ } else if (preset === 'HIGHRISK') {
|
|
|
+ if (searchInput) searchInput.value = 'China';
|
|
|
+ }
|
|
|
+ renderGeoCountries();
|
|
|
+}
|
|
|
+
|
|
|
async function toggleGeoCountry(code, enabled) {
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/firewall/geoip/countries/${encodeURIComponent(code)}/toggle`, {
|
|
|
@@ -3358,6 +3411,21 @@ async function toggleGeoCountry(code, enabled) {
|
|
|
showToast('Error', err.message, 'error');
|
|
|
}
|
|
|
}
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ enabled })
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast(enabled ? 'Country Enabled' : 'Country Disabled', `${code} ${enabled ? 'active in firewall' : 'removed from firewall'}`, 'success');
|
|
|
+ loadGeoIP();
|
|
|
+ } else {
|
|
|
+ const err = await res.json();
|
|
|
+ showToast('Error', err.message || 'Failed to toggle country', 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
async function updateGeoCountry(code) {
|
|
|
try {
|