// State let currentPage = 1; let totalPages = 1; let searchTimeout = null; let deleteEntryId = null; let selectedFile = null; let clientLogos = {}; let auditPage = 1; // Initialize document.addEventListener('DOMContentLoaded', () => { // Only run on index page (has entries table) if (document.getElementById('entriesTableBody')) { loadEntries(); loadStats(); } loadClientLogos(); loadAndApplyWhitelabel(); // Enable clear all button when "DELETE" is typed const confirmClearInput = document.getElementById('confirmClearInput'); if (confirmClearInput) { confirmClearInput.addEventListener('input', (e) => { document.getElementById('confirmClearBtn').disabled = e.target.value !== 'DELETE'; }); } }); // Load and apply whitelabel settings on page load async function loadAndApplyWhitelabel() { try { const result = await api('whitelabel_get'); if (result.success && result.settings) { applyWhitelabelSettings(result.settings); } } catch (error) { console.error('Failed to load whitelabel settings:', error); } } // Tab switching function switchTab(tab) { document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); // Try to find and activate the corresponding tab button (may not exist for settings tabs) const tabButton = document.querySelector(`.tab[onclick="switchTab('${tab}')"]`); if (tabButton) { tabButton.classList.add('active'); } document.getElementById(`tab-${tab}`).classList.add('active'); // Load data for advanced tab if (tab === 'advanced') { loadShortnames(); loadLogosGrid(); } // Load data for integrations tab if (tab === 'integrations') { loadWebhookSettings(); loadWebhookQueueStatus(); loadIpRegistrySettings(); loadAwsSettings(); } // Load data for audit tab if (tab === 'audit') { loadAuditLog(); } // Load data for developer tab if (tab === 'developer') { loadSystemInfo(); loadErrorLogs(); } // Load data for PTR tab if (tab === 'ptr') { loadPtrZones(); } // Load data for whitelabel tab if (tab === 'whitelabel') { loadWhitelabelSettings(); } } // Settings menu functions function toggleSettingsMenu() { const menu = document.getElementById('settingsMenu'); const btn = document.getElementById('settingsBtn'); menu.classList.toggle('active'); btn.classList.toggle('active'); } function closeSettingsMenu() { const menu = document.getElementById('settingsMenu'); const btn = document.getElementById('settingsBtn'); menu.classList.remove('active'); btn.classList.remove('active'); } // Close settings menu when clicking outside document.addEventListener('click', function(e) { const dropdown = document.querySelector('.settings-dropdown'); if (dropdown && !dropdown.contains(e.target)) { closeSettingsMenu(); } }); // API Helper async function api(action, params = {}, method = 'GET', body = null) { const url = new URL('api.php', window.location.href); url.searchParams.set('action', action); Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v)); const options = { method }; if (body) { options.headers = { 'Content-Type': 'application/json' }; options.body = JSON.stringify({ ...body, csrf_token: (typeof CSRF_TOKEN !== 'undefined' ? CSRF_TOKEN : '') }); } const response = await fetch(url, options); const text = await response.text(); if (!text || text.trim() === '') { return { success: false, error: 'Empty response from server' }; } try { return JSON.parse(text); } catch (e) { console.error('API response parse error:', text); return { success: false, error: 'Invalid JSON response: ' + text.substring(0, 200) }; } } // Load client logos async function loadClientLogos() { try { const result = await api('logos_list'); if (result.success) { clientLogos = {}; result.data.forEach(logo => { clientLogos[logo.short_name] = logo.logo_url; }); } } catch (error) { console.error('Failed to load logos:', error); } } // Load entries async function loadEntries(page = 1) { const tableContent = document.getElementById('tableContent'); const searchInput = document.getElementById('searchInput'); if (!tableContent) return; // Not on index page currentPage = page; const searchQuery = searchInput ? searchInput.value : ''; tableContent.innerHTML = '
Get started by adding your first geofeed entry or import from the Advanced tab.
| IP Prefix | Client | Country | Actions |
|---|---|---|---|
| ${escapeHtml(entry.ip_prefix)} |
${entry.client_short_name ? `
${clientLogos[entry.client_short_name]
? `
` : '-'}
|
${entry.country_code ? ` ${getFlagEmoji(entry.country_code)} ${escapeHtml(entry.country_code)} ` : '-'} |
|
Failed to load audit log
Network error
No audit log entries yet
Shortnames are automatically created when you add entries with a client short name.
`; } else { container.innerHTML = 'No client shortnames found. Add entries with a client short name to create shortnames.
'; } } catch (error) { container.innerHTML = 'Failed to load shortnames
'; } } // Load logos grid async function loadLogosGrid() { const grid = document.getElementById('logoGrid'); if (!grid) return; try { const result = await api('logos_list'); if (result.success && result.data.length > 0) { grid.innerHTML = result.data.map(logo => `No logos configured yet. Add a logo above to get started.
'; } } catch (error) { grid.innerHTML = 'Failed to load logos
'; } } // Save logo async function saveLogo() { const shortName = document.getElementById('logoShortName').value.trim(); const logoUrl = document.getElementById('logoUrl').value.trim(); if (!shortName || !logoUrl) { showToast('Please enter both short name and logo URL', 'error'); return; } try { const result = await api('logo_save', {}, 'POST', { short_name: shortName, logo_url: logoUrl }); if (result.success) { showToast('Logo saved successfully', 'success'); document.getElementById('logoShortName').value = ''; document.getElementById('logoUrl').value = ''; loadLogosGrid(); loadClientLogos(); loadEntries(currentPage); } else { showToast(result.error || 'Failed to save logo', 'error'); } } catch (error) { showToast('Network error', 'error'); } } // Edit logo function editLogo(shortName, logoUrl) { document.getElementById('logoShortName').value = shortName; document.getElementById('logoUrl').value = logoUrl; document.getElementById('logoShortName').scrollIntoView({ behavior: 'smooth', block: 'center' }); } // Delete logo async function deleteLogo(shortName) { if (!confirm(`Delete logo for "${shortName}"?`)) return; try { const result = await api('logo_delete', {}, 'POST', { short_name: shortName }); if (result.success) { showToast('Logo deleted successfully', 'success'); loadLogosGrid(); loadClientLogos(); loadEntries(currentPage); } else { showToast(result.error || 'Failed to delete logo', 'error'); } } catch (error) { showToast('Network error', 'error'); } } // Load webhook settings async function loadWebhookSettings() { try { const result = await api('webhook_settings_get'); if (result.success) { document.getElementById('webhookUrl').value = result.data.webhook_url || ''; document.getElementById('webhookEnabled').checked = result.data.webhook_enabled; document.getElementById('webhookDelay').value = result.data.webhook_delay_minutes || 3; } } catch (error) { console.error('Failed to load webhook settings:', error); } } // Load IP Registry settings async function loadIpRegistrySettings() { try { const result = await api('ipregistry_settings_get'); if (result.success) { document.getElementById('ipRegistryEnabled').checked = result.data.enabled; if (result.data.api_key_masked) { document.getElementById('ipRegistryApiKey').placeholder = `Current: ${result.data.api_key_masked}`; } else if (result.data.has_env_key) { document.getElementById('ipRegistryApiKey').placeholder = 'Using environment variable'; } } } catch (error) { console.error('Failed to load IP Registry settings:', error); } } // Save IP Registry settings async function saveIpRegistrySettings() { const enabled = document.getElementById('ipRegistryEnabled').checked; const apiKey = document.getElementById('ipRegistryApiKey').value.trim(); try { const result = await api('ipregistry_settings_save', {}, 'POST', { enabled: enabled, api_key: apiKey }); if (result.success) { showToast('IP Registry settings saved successfully', 'success'); document.getElementById('ipRegistryApiKey').value = ''; loadIpRegistrySettings(); } else { showToast(result.error || 'Failed to save settings', 'error'); } } catch (error) { showToast('Network error', 'error'); } } // Save webhook settings async function saveWebhookSettings() { const webhookUrl = document.getElementById('webhookUrl').value.trim(); const webhookEnabled = document.getElementById('webhookEnabled').checked; const webhookDelay = parseInt(document.getElementById('webhookDelay').value) || 3; try { const result = await api('webhook_settings_save', {}, 'POST', { webhook_url: webhookUrl, webhook_enabled: webhookEnabled, webhook_delay_minutes: webhookDelay }); if (result.success) { showToast('Webhook settings saved successfully', 'success'); } else { showToast(result.error || 'Failed to save settings', 'error'); } } catch (error) { showToast('Network error', 'error'); } } // Test webhook connection async function testWebhook() { const webhookUrl = document.getElementById('webhookUrl').value.trim(); if (!webhookUrl) { showToast('Please enter a webhook URL first', 'error'); return; } try { showToast('Testing webhook...', 'success'); const result = await api('webhook_test', {}, 'POST', {}); if (result.success) { showToast(`Webhook test successful (HTTP ${result.http_code})`, 'success'); } else { showToast(result.error || 'Webhook test failed', 'error'); } } catch (error) { showToast('Network error', 'error'); } } // Trigger webhook immediately async function triggerWebhookNow() { if (!confirm('This will immediately trigger the n8n webhook to update the CDN. Continue?')) return; try { const result = await api('webhook_trigger', {}, 'POST', {}); if (result.success) { showToast('Webhook triggered successfully', 'success'); loadWebhookQueueStatus(); } else { showToast(result.error || 'Failed to trigger webhook', 'error'); } } catch (error) { showToast('Network error', 'error'); } } // Load webhook queue status async function loadWebhookQueueStatus() { const container = document.getElementById('webhookQueueContainer'); try { const result = await api('webhook_queue_status'); if (result.success) { renderWebhookQueueStatus(result.data); } else { container.innerHTML = 'Failed to load queue status
'; } } catch (error) { container.innerHTML = 'Network error
'; } } // Render webhook queue status function renderWebhookQueueStatus(data) { const container = document.getElementById('webhookQueueContainer'); // Stats row let html = `No webhook activity yet
'; } container.innerHTML = html; } // Clear webhook queue async function clearWebhookQueue() { const clearType = document.getElementById('clearQueueType')?.value || 'pending'; const typeLabels = { 'pending': 'pending webhooks', 'failed': 'failed webhooks', 'all': 'all webhook history' }; if (!confirm(`Are you sure you want to clear ${typeLabels[clearType]}?`)) { return; } try { const result = await api('webhook_queue_clear', { csrf_token: CSRF_TOKEN, clear_type: clearType }); if (result.success) { showToast(`${result.message} (${result.deleted} removed)`, 'success'); loadWebhookQueueStatus(); } else { showToast(result.error || 'Failed to clear queue', 'error'); } } catch (error) { showToast('Network error', 'error'); } } // Get time until a future date function getTimeUntil(date) { const seconds = Math.floor((date - new Date()) / 1000); if (seconds < 0) return 'now'; if (seconds < 60) return `in ${seconds}s`; if (seconds < 3600) return `in ${Math.floor(seconds / 60)}m`; return `in ${Math.floor(seconds / 3600)}h`; } // Time ago helper function getTimeAgo(date) { const seconds = Math.floor((new Date() - date) / 1000); if (seconds < 60) return 'just now'; if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago'; if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago'; if (seconds < 604800) return Math.floor(seconds / 86400) + 'd ago'; return date.toLocaleDateString(); } // Search with debounce function debounceSearch() { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => loadEntries(1), 300); } // Modal functions function openModal(entry = null) { const modal = document.getElementById('entryModal'); const form = document.getElementById('entryForm'); const title = document.getElementById('modalTitle'); const enrichSection = document.getElementById('enrichmentSection'); const enrichStatus = document.getElementById('enrichmentStatus'); const enrichData = document.getElementById('enrichmentData'); const route53Section = document.getElementById('route53Section'); const route53Content = document.getElementById('route53Content'); form.reset(); if (entry) { title.textContent = 'Edit Entry'; document.getElementById('entryId').value = entry.id; document.getElementById('ipPrefix').value = entry.ip_prefix || ''; document.getElementById('countryCode').value = entry.country_code || ''; document.getElementById('regionCode').value = entry.region_code || ''; document.getElementById('city').value = entry.city || ''; document.getElementById('postalCode').value = entry.postal_code || ''; document.getElementById('clientShortName').value = entry.client_short_name || ''; document.getElementById('notes').value = entry.notes || ''; // Show enrichment section when editing enrichSection.style.display = 'block'; if (entry.ipr_enriched_at) { const enrichedDate = new Date(entry.ipr_enriched_at).toLocaleString(); enrichStatus.innerHTML = `Enriched on ${enrichedDate}`; enrichData.style.display = 'block'; // Populate enrichment data document.getElementById('enrichIsp').textContent = entry.ipr_isp || '-'; document.getElementById('enrichAsn').textContent = entry.ipr_asn ? `AS${entry.ipr_asn}` : '-'; document.getElementById('enrichOrg').textContent = entry.ipr_org || '-'; document.getElementById('enrichType').textContent = entry.ipr_connection_type || '-'; // Show active flags const flags = []; if (entry.flag_abuser == 1) flags.push('Abuser'); if (entry.flag_attacker == 1) flags.push('Attacker'); if (entry.flag_bogon == 1) flags.push('Bogon'); if (entry.flag_cloud_provider == 1) flags.push('Cloud'); if (entry.flag_proxy == 1) flags.push('Proxy'); if (entry.flag_relay == 1) flags.push('Relay'); if (entry.flag_tor == 1) flags.push('Tor'); if (entry.flag_tor_exit == 1) flags.push('Tor Exit'); if (entry.flag_vpn == 1) flags.push('VPN'); if (entry.flag_anonymous == 1) flags.push('Anonymous'); if (entry.flag_threat == 1) flags.push('Threat'); const flagsEl = document.getElementById('enrichFlags'); if (flags.length > 0) { flagsEl.innerHTML = 'Flags: ' + flags.map(f => `${f}`).join(''); } else { flagsEl.innerHTML = 'Flags: None'; } } else { enrichStatus.innerHTML = 'Not enriched yet'; enrichData.style.display = 'none'; } // Reset button state document.getElementById('reEnrichBtn').disabled = false; document.getElementById('reEnrichBtnText').textContent = 'Re-enrich IP'; // Show Route53 section and load records route53Section.style.display = 'block'; route53Content.innerHTML = '${escapeHtml(rec.ip_address)}
${escapeHtml(value || '-')}` : displayVal}Not enriched yet. Click the globe icon to enrich this entry.
` : `Security flags will be available after enrichment.
` : `| Hostname | IP Address | PTR Status |
|---|---|---|
${escapeHtml(rec.hostname)}
|
${escapeHtml(rec.ip_address)}
|
${rec.ptr_status || 'unknown'} |
No Route53 A records found for this prefix.
`; } } catch (error) { container.innerHTML = `Could not load Route53 records.
`; } } function closeInfoModal() { document.getElementById('infoModal').classList.remove('active'); } // Delete entry function deleteEntry(id, prefix) { deleteEntryId = id; document.getElementById('deletePrefix').textContent = prefix; document.getElementById('deleteModal').classList.add('active'); } function closeDeleteModal() { document.getElementById('deleteModal').classList.remove('active'); deleteEntryId = null; } async function confirmDelete() { if (!deleteEntryId) return; try { const result = await api('delete', {}, 'POST', { id: deleteEntryId }); if (result.success) { showToast('Entry deleted successfully', 'success'); closeDeleteModal(); loadEntries(currentPage); loadStats(); } else { showToast(result.error || 'Failed to delete entry', 'error'); } } catch (error) { showToast('Network error', 'error'); } } // Export CSV function exportCSV() { window.location.href = 'api.php?action=export&format=download'; } // Export Audit Log as CSV async function exportAuditLog() { showToast('Preparing audit log export...', 'info'); try { // Fetch all audit log entries (high limit to get all) const result = await api('audit_log', { page: 1, limit: 10000 }); if (!result.success || !result.data || result.data.length === 0) { showToast('No audit log entries to export', 'error'); return; } // Build CSV content const headers = ['Date/Time', 'Action', 'IP Prefix', 'Details', 'Changed By']; const rows = result.data.map(entry => { const date = new Date(entry.changed_at).toISOString(); const action = entry.action; let ipPrefix = ''; let details = ''; if (entry.new_values?.type === 'bulk_import') { details = `Bulk import: ${entry.new_values.inserted} inserted, ${entry.new_values.updated} updated`; } else if (entry.new_values?.type === 'url_import') { details = `URL import from ${entry.new_values.url}: ${entry.new_values.inserted} inserted, ${entry.new_values.updated} updated`; } else if (entry.new_values?.type === 'clear_all') { details = `Cleared ${entry.old_values?.count || 0} entries`; } else { ipPrefix = entry.ip_prefix || entry.old_values?.ip_prefix || entry.new_values?.ip_prefix || ''; // Build details from old/new values if (entry.action === 'UPDATE' && entry.old_values && entry.new_values) { const changes = []; for (const key of Object.keys(entry.new_values)) { if (entry.old_values[key] !== entry.new_values[key]) { changes.push(`${key}: ${entry.old_values[key] || '(empty)'} → ${entry.new_values[key] || '(empty)'}`); } } details = changes.join('; '); } else if (entry.action === 'INSERT' && entry.new_values) { details = Object.entries(entry.new_values) .filter(([k, v]) => v && k !== 'ip_prefix') .map(([k, v]) => `${k}: ${v}`) .join('; '); } else if (entry.action === 'DELETE' && entry.old_values) { details = Object.entries(entry.old_values) .filter(([k, v]) => v && k !== 'ip_prefix') .map(([k, v]) => `${k}: ${v}`) .join('; '); } } const changedBy = entry.changed_by || 'Unknown'; return [date, action, ipPrefix, details, changedBy]; }); // Create CSV string const csvContent = [ headers.join(','), ...rows.map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',')) ].join('\n'); // Download file const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `audit-log-${new Date().toISOString().split('T')[0]}.csv`; link.click(); URL.revokeObjectURL(link.href); showToast(`Exported ${result.data.length} audit log entries`, 'success'); } catch (error) { console.error('Audit log export error:', error); showToast('Failed to export audit log', 'error'); } } // File handling function handleFileSelect(input) { if (input.files && input.files[0]) { selectedFile = input.files[0]; document.getElementById('fileName').textContent = selectedFile.name; document.getElementById('uploadBtn').disabled = false; // Reset results document.getElementById('fileResults').classList.remove('active', 'success', 'error'); } } // Import from file async function importFromFile() { if (!selectedFile) return; const btn = document.getElementById('uploadBtn'); const progress = document.getElementById('fileProgress'); const progressFill = document.getElementById('fileProgressFill'); const progressText = document.getElementById('fileProgressText'); const results = document.getElementById('fileResults'); btn.disabled = true; progress.classList.add('active'); results.classList.remove('active', 'success', 'error'); try { progressText.textContent = 'Reading file...'; progressFill.style.width = '20%'; const text = await selectedFile.text(); progressText.textContent = 'Parsing CSV...'; progressFill.style.width = '40%'; const entries = parseCSV(text); progressText.textContent = `Importing ${entries.length} entries...`; progressFill.style.width = '60%'; const result = await api('import', {}, 'POST', { entries, csrf_token: csrfToken }); progressFill.style.width = '100%'; if (result.success) { results.classList.add('active', 'success'); document.getElementById('fileResultsText').textContent = `Successfully imported ${result.inserted} new entries, updated ${result.updated} existing entries.`; showToast('Import completed successfully', 'success'); loadEntries(); loadStats(); } else { results.classList.add('active', 'error'); document.getElementById('fileResultsText').textContent = result.error || 'Import failed'; showToast(result.error || 'Import failed', 'error'); } } catch (error) { results.classList.add('active', 'error'); document.getElementById('fileResultsText').textContent = error.message; showToast('Import failed: ' + error.message, 'error'); } finally { btn.disabled = false; setTimeout(() => { progress.classList.remove('active'); progressFill.style.width = '0%'; }, 1000); } } // Import from URL async function importFromUrl() { const url = document.getElementById('importUrl').value.trim(); if (!url) { showToast('Please enter a URL', 'error'); return; } const btn = document.getElementById('urlImportBtn'); const progress = document.getElementById('urlProgress'); const progressFill = document.getElementById('urlProgressFill'); const progressText = document.getElementById('urlProgressText'); const results = document.getElementById('urlResults'); btn.disabled = true; progress.classList.add('active'); results.classList.remove('active', 'success', 'error'); try { progressText.textContent = 'Fetching data...'; progressFill.style.width = '30%'; const result = await api('import_url', {}, 'POST', { url, csrf_token: csrfToken }); progressFill.style.width = '100%'; if (result.success) { results.classList.add('active', 'success'); document.getElementById('urlResultsText').textContent = `Successfully imported ${result.inserted} new entries, updated ${result.updated} existing entries.`; showToast('Import completed successfully', 'success'); loadEntries(); loadStats(); } else { results.classList.add('active', 'error'); document.getElementById('urlResultsText').textContent = result.error || 'Import failed'; showToast(result.error || 'Import failed', 'error'); } } catch (error) { results.classList.add('active', 'error'); document.getElementById('urlResultsText').textContent = error.message; showToast('Import failed: ' + error.message, 'error'); } finally { btn.disabled = false; setTimeout(() => { progress.classList.remove('active'); progressFill.style.width = '0%'; }, 1000); } } // Parse CSV function parseCSV(text) { const lines = text.split(/\r?\n/); const entries = []; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const parts = trimmed.split(','); if (parts.length < 1 || !parts[0].trim()) continue; entries.push({ ip_prefix: parts[0]?.trim() || '', country_code: parts[1]?.trim().toUpperCase() || '', region_code: parts[2]?.trim().toUpperCase() || '', city: parts[3]?.trim() || '', postal_code: parts[4]?.trim() || '' }); } return entries; } // Clear all function confirmClearAll() { document.getElementById('confirmClearInput').value = ''; document.getElementById('confirmClearBtn').disabled = true; document.getElementById('clearAllModal').classList.add('active'); } function closeClearAllModal() { document.getElementById('clearAllModal').classList.remove('active'); } async function executeClearAll() { if (document.getElementById('confirmClearInput').value !== 'DELETE') return; try { const result = await api('clear_all', {}, 'POST', { csrf_token: csrfToken }); if (result.success) { showToast('All entries cleared successfully', 'success'); closeClearAllModal(); loadEntries(); loadStats(); } else { showToast(result.error || 'Failed to clear entries', 'error'); } } catch (error) { showToast('Network error', 'error'); } } // Toast notifications function showToast(message, type = 'success') { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); toast.className = `toast ${type}`; const icon = type === 'success' ? '' : ''; toast.innerHTML = ` `; container.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideIn 0.3s ease reverse'; setTimeout(() => toast.remove(), 300); }, 4000); } // Helpers function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function getFlagEmoji(countryCode) { if (!countryCode || countryCode.length !== 2) return ''; const codePoints = countryCode .toUpperCase() .split('') .map(char => 127397 + char.charCodeAt()); return String.fromCodePoint(...codePoints); } // Render security flags function renderSecurityFlags(entry) { const flags = []; // Danger flags (red) if (entry.flag_abuser == 1) flags.push({label: 'Abuser', type: 'danger'}); if (entry.flag_attacker == 1) flags.push({label: 'Attacker', type: 'danger'}); if (entry.flag_threat == 1) flags.push({label: 'Threat', type: 'danger'}); if (entry.flag_tor_exit == 1) flags.push({label: 'Tor Exit', type: 'danger'}); // Warning flags (yellow) if (entry.flag_proxy == 1) flags.push({label: 'Proxy', type: 'warning'}); if (entry.flag_vpn == 1) flags.push({label: 'VPN', type: 'warning'}); if (entry.flag_tor == 1 && entry.flag_tor_exit != 1) flags.push({label: 'Tor', type: 'warning'}); if (entry.flag_relay == 1) flags.push({label: 'Relay', type: 'warning'}); if (entry.flag_anonymous == 1) flags.push({label: 'Anon', type: 'warning'}); // Info flags (blue) if (entry.flag_cloud_provider == 1) flags.push({label: 'Cloud', type: 'info'}); if (entry.flag_bogon == 1) flags.push({label: 'Bogon', type: 'info'}); if (flags.length === 0) { return entry.ipr_enriched_at ? 'Clean' : '-'; } return flags.map(f => `${f.label}`).join(''); } // Enrich single IP async function enrichIp(id) { try { const result = await api('enrich_ip', {}, 'POST', { id }); if (result.success) { showToast('IP enriched successfully', 'success'); loadEntries(currentPage); } else { showToast(result.error || 'Failed to enrich IP', 'error'); } } catch (error) { showToast('Network error', 'error'); } } // Enrich all un-enriched IPs async function enrichAllIps(btn) { btn.disabled = true; btn.innerHTML = ' Enriching...'; try { const result = await api('enrich_all', {}, 'POST', {}); if (result.success) { showToast(`Enriched ${result.enriched} IPs. ${result.failed || 0} failed.`, 'success'); loadEntries(currentPage); } else { showToast(result.error || 'Failed to enrich IPs', 'error'); } } catch (error) { showToast('Network error', 'error'); } finally { btn.disabled = false; btn.innerHTML = 'Enrich All Un-enriched IPs'; } } // Re-enrich current entry from edit modal async function reEnrichCurrentEntry() { const id = document.getElementById('entryId').value; if (!id) return; const btn = document.getElementById('reEnrichBtn'); const btnText = document.getElementById('reEnrichBtnText'); btn.disabled = true; btnText.textContent = 'Enriching...'; try { const result = await api('enrich_ip', {}, 'POST', { id: parseInt(id) }); if (result.success) { showToast('IP enriched successfully', 'success'); // Reload the entry data to update the modal const entryResult = await api('get', { id }); if (entryResult.success) { // Update enrichment display without closing modal const entry = entryResult.data; const enrichStatus = document.getElementById('enrichmentStatus'); const enrichData = document.getElementById('enrichmentData'); const enrichedDate = new Date(entry.ipr_enriched_at).toLocaleString(); enrichStatus.innerHTML = `Enriched on ${enrichedDate}`; enrichData.style.display = 'block'; document.getElementById('enrichIsp').textContent = entry.ipr_isp || '-'; document.getElementById('enrichAsn').textContent = entry.ipr_asn ? `AS${entry.ipr_asn}` : '-'; document.getElementById('enrichOrg').textContent = entry.ipr_org || '-'; document.getElementById('enrichType').textContent = entry.ipr_connection_type || '-'; // Update flags const flags = []; if (entry.flag_abuser == 1) flags.push('Abuser'); if (entry.flag_attacker == 1) flags.push('Attacker'); if (entry.flag_bogon == 1) flags.push('Bogon'); if (entry.flag_cloud_provider == 1) flags.push('Cloud'); if (entry.flag_proxy == 1) flags.push('Proxy'); if (entry.flag_relay == 1) flags.push('Relay'); if (entry.flag_tor == 1) flags.push('Tor'); if (entry.flag_tor_exit == 1) flags.push('Tor Exit'); if (entry.flag_vpn == 1) flags.push('VPN'); if (entry.flag_anonymous == 1) flags.push('Anonymous'); if (entry.flag_threat == 1) flags.push('Threat'); const flagsEl = document.getElementById('enrichFlags'); if (flags.length > 0) { flagsEl.innerHTML = 'Flags: ' + flags.map(f => `${f}`).join(''); } else { flagsEl.innerHTML = 'Flags: None'; } } // Also refresh the main table loadEntries(currentPage); } else { showToast(result.error || 'Failed to enrich IP', 'error'); } } catch (error) { showToast('Network error', 'error'); } finally { btn.disabled = false; btnText.textContent = 'Re-enrich IP'; } } // Developer Tab Functions // Download database backup function downloadDatabaseBackup() { const url = `api.php?action=database_backup`; window.location.href = url; showToast('Downloading backup...', 'success'); } // Import database backup async function importDatabaseBackup() { const fileInput = document.getElementById('dbImportFile'); const file = fileInput.files[0]; if (!file) { showToast('Please select a backup file', 'error'); return; } if (!file.name.endsWith('.json')) { showToast('Please select a valid JSON backup file', 'error'); return; } if (!confirm('WARNING: This will replace ALL existing data with the backup. Are you sure you want to continue?')) { return; } try { const text = await file.text(); const backupData = JSON.parse(text); // Validate basic structure if (!backupData.backup_info || !backupData.geofeed_entries) { showToast('Invalid backup file format', 'error'); return; } const result = await api('database_import', {}, 'POST', { backup_data: backupData }); if (result.success) { showToast(`Import successful: ${result.imported.entries} entries, ${result.imported.settings} settings, ${result.imported.logos} logos`, 'success'); fileInput.value = ''; loadSystemInfo(); loadEntries(); loadStats(); } else { showToast(result.error || 'Import failed', 'error'); } } catch (e) { if (e instanceof SyntaxError) { showToast('Invalid JSON file', 'error'); } else { showToast('Import failed: ' + e.message, 'error'); } } } // Load system information async function loadSystemInfo() { const container = document.getElementById('systemInfoContent'); container.innerHTML = '| App Version | ${escapeHtml(data.app_version)} |
| PHP Version | ${escapeHtml(data.php_version)} |
| Server Software | ${escapeHtml(data.server.software)} |
| Server Time | ${escapeHtml(data.server.time)} |
| Timezone | ${escapeHtml(data.server.timezone)} |
| Database Name | ${escapeHtml(data.database.name)} |
| Database Host | ${escapeHtml(data.database.host)} |
| Database Size | ${data.database.size_mb} MB |
| Geofeed Entries | ${data.database.entries.toLocaleString()} |
| Enriched Entries | ${data.database.enriched_entries.toLocaleString()} |
| Settings | ${data.database.settings.toLocaleString()} |
| Audit Log Entries | ${data.database.audit_log_entries.toLocaleString()} |
| Client Logos | ${data.database.client_logos.toLocaleString()} |
| Table Name |
|---|
${escapeHtml(t)} |
| Table | Column | Definition |
|---|---|---|
${escapeHtml(c.table)} |
${escapeHtml(c.column)} |
${escapeHtml(c.definition)} |
| Table | Index Name | Columns |
|---|---|---|
${escapeHtml(i.table)} |
${escapeHtml(i.index)} |
${escapeHtml(i.columns)} |
| ${escapeHtml(a)} |
| ${escapeHtml(f)} |
Log path: ${escapeHtml(result.log_path)}
${escapeHtml(result.log_path)}
(${escapeHtml(result.log_size_formatted)})
| Type | Timestamp | Message |
|---|---|---|
| ${getTypeBadge(entry.type)} | ${entry.timestamp ? escapeHtml(entry.timestamp) : '-'} | ${escapeHtml(entry.message)} |
${escapeHtml(record.ip_address)}