// 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 = '
'; try { const result = await api('list', { page, search: searchQuery }); if (result.success) { renderTable(result.data, result.pagination); } else { showToast(result.error || 'Failed to load entries', 'error'); } } catch (error) { showToast('Network error', 'error'); } } // Render table function renderTable(entries, pagination) { totalPages = pagination.pages; if (entries.length === 0) { document.getElementById('tableContent').innerHTML = `

No entries found

Get started by adding your first geofeed entry or import from the Advanced tab.

`; return; } const tableHTML = `
${entries.map(entry => ` `).join('')}
IP Prefix Client Hostname Country Region City ISP Flags Actions
${escapeHtml(entry.ip_prefix)} ${entry.client_short_name ? `
${clientLogos[entry.client_short_name] ? `` : `${escapeHtml(entry.client_short_name.charAt(0).toUpperCase())}` }
` : '-'}
${entry.ipr_hostname ? `${escapeHtml(entry.ipr_hostname)}` : '-'} ${entry.country_code ? ` ${getFlagEmoji(entry.country_code)} ${escapeHtml(entry.country_code)} ` : '-'} ${entry.region_code ? `${escapeHtml(entry.region_code)}` : '-'} ${entry.city ? `${escapeHtml(entry.city)}` : '-'} ${entry.ipr_isp ? `${escapeHtml(entry.ipr_isp)}` : '-'}
${renderSecurityFlags(entry)}
`; document.getElementById('tableContent').innerHTML = tableHTML; } // Load stats async function loadStats() { const statTotal = document.getElementById('statTotal'); if (!statTotal) return; // Not on index page try { const result = await api('stats'); if (result.success) { statTotal.textContent = result.data.total_entries.toLocaleString(); document.getElementById('statIPv4').textContent = (result.data.ip_versions?.ipv4 || 0).toLocaleString(); document.getElementById('statIPv6').textContent = (result.data.ip_versions?.ipv6 || 0).toLocaleString(); document.getElementById('statCountries').textContent = result.data.by_country?.length || 0; } } catch (error) { console.error('Failed to load stats:', error); } } // Load audit log async function loadAuditLog(page = 1) { auditPage = page; const container = document.getElementById('auditLogContainer'); if (!container) return; container.innerHTML = '
'; try { const result = await api('audit_log', { page, limit: 20 }); if (result.success) { renderAuditLog(result.data, result.pagination); } else { container.innerHTML = '

Failed to load audit log

'; } } catch (error) { container.innerHTML = '

Network error

'; } } // Render audit log function renderAuditLog(entries, pagination) { const container = document.getElementById('auditLogContainer'); if (entries.length === 0) { container.innerHTML = '

No audit log entries yet

'; document.getElementById('auditPagination').style.display = 'none'; return; } container.innerHTML = entries.map(entry => { const actionText = { 'INSERT': 'Created', 'UPDATE': 'Updated', 'DELETE': 'Deleted' }[entry.action] || entry.action; const icon = { 'INSERT': '', 'UPDATE': '', 'DELETE': '' }[entry.action] || ''; 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 if (entry.ip_prefix) { details = entry.ip_prefix; } else if (entry.old_values?.ip_prefix) { details = entry.old_values.ip_prefix; } else if (entry.new_values?.ip_prefix) { details = entry.new_values.ip_prefix; } const date = new Date(entry.changed_at); const timeAgo = getTimeAgo(date); return `
${icon}
${actionText} ${details && !entry.new_values?.type ? `${escapeHtml(details)}` : ''} ${timeAgo}
${entry.new_values?.type ? `
${escapeHtml(details)}
` : ''}
by ${escapeHtml(entry.changed_by || 'Unknown')}
`; }).join(''); // Render pagination const paginationEl = document.getElementById('auditPagination'); if (pagination.pages > 1) { paginationEl.style.display = 'flex'; paginationEl.innerHTML = ` Page ${auditPage} of ${pagination.pages} `; } else { paginationEl.style.display = 'none'; } } // Load shortnames for logo dropdown async function loadShortnames() { const select = document.getElementById('logoShortName'); if (!select) return; try { const result = await api('shortnames_list'); // Keep first option select.innerHTML = ''; if (result.success && result.data.length > 0) { result.data.forEach(name => { const option = document.createElement('option'); option.value = name; option.textContent = name; select.appendChild(option); }); } } catch (error) { console.error('Failed to load shortnames:', error); } } // Load shortnames grid for settings page async function loadShortnamesGrid() { const container = document.getElementById('shortnamesContainer'); if (!container) return; try { const result = await api('shortnames_list'); if (result.success && result.data.length > 0) { container.innerHTML = `
${result.data.map(name => ` ${escapeHtml(name)} `).join('')}

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 => `
${escapeHtml(logo.short_name)}
${escapeHtml(logo.short_name)}
${escapeHtml(logo.logo_url)}
`).join(''); } else { grid.innerHTML = '

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 = `
${data.counts.pending} Pending
${data.counts.completed_24h} Completed (24h)
${data.counts.failed_24h} Failed (24h)
`; // Pending webhooks if (data.pending.length > 0) { html += '

Pending Webhooks

'; html += '
'; data.pending.forEach(item => { const scheduledFor = new Date(item.scheduled_for); const timeUntil = getTimeUntil(scheduledFor); html += `
${escapeHtml(item.trigger_reason || 'Queued update')}
${item.entries_affected} entries affected • fires ${timeUntil}
`; }); html += '
'; } // Recent webhooks if (data.recent.length > 0) { html += '

Recent Webhooks

'; data.recent.forEach(item => { const isSuccess = item.status === 'completed'; const processedAt = item.processed_at ? getTimeAgo(new Date(item.processed_at)) : '-'; html += `
${isSuccess ? '' : '' }
${escapeHtml(item.trigger_reason || 'Update')}
${item.entries_affected} entries • HTTP ${item.response_code || '-'} • ${processedAt}
`; }); } if (data.pending.length === 0 && data.recent.length === 0) { 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 = '
Loading...
'; loadRoute53RecordsForEdit(entry.ip_prefix); } else { title.textContent = 'Add Entry'; document.getElementById('entryId').value = ''; enrichSection.style.display = 'none'; route53Section.style.display = 'none'; } modal.classList.add('active'); setTimeout(() => document.getElementById('ipPrefix').focus(), 100); } // Load Route53 records for edit modal async function loadRoute53RecordsForEdit(ipPrefix) { const container = document.getElementById('route53Content'); if (!container) return; try { const result = await api('route53_records_by_prefix', { ip_prefix: ipPrefix }); if (result.success && result.records && result.records.length > 0) { const maxDisplay = 5; const displayRecords = result.records.slice(0, maxDisplay); const hasMore = result.records.length > maxDisplay; container.innerHTML = `
${displayRecords.map(rec => `
${escapeHtml(rec.hostname)} ${escapeHtml(rec.ip_address)}
${rec.ptr_status || 'unknown'}
`).join('')}
${hasMore ? `
+ ${result.records.length - maxDisplay} more records
` : ''}
${result.count} A record${result.count !== 1 ? 's' : ''} in this prefix
`; } else { container.innerHTML = `
No Route53 A records found for this prefix.
`; } } catch (error) { container.innerHTML = `
Could not load Route53 records.
`; } } function closeModal() { document.getElementById('entryModal').classList.remove('active'); } // Edit entry async function editEntry(id) { try { const result = await api('get', { id }); if (result.success) { openModal(result.data); } else { showToast(result.error || 'Failed to load entry', 'error'); } } catch (error) { showToast('Network error', 'error'); } } // Save entry async function saveEntry(event) { event.preventDefault(); const id = document.getElementById('entryId').value; const action = id ? 'update' : 'create'; const data = { id: id || undefined, ip_prefix: document.getElementById('ipPrefix').value, country_code: document.getElementById('countryCode').value.toUpperCase(), region_code: document.getElementById('regionCode').value.toUpperCase(), city: document.getElementById('city').value, postal_code: document.getElementById('postalCode').value, client_short_name: document.getElementById('clientShortName').value, notes: document.getElementById('notes').value }; try { const result = await api(action, {}, 'POST', data); if (result.success) { showToast(result.message || 'Entry saved successfully', 'success'); closeModal(); loadEntries(currentPage); loadStats(); } else { showToast(result.error || 'Failed to save entry', 'error'); } } catch (error) { showToast('Network error', 'error'); } } // Show entry info modal async function showEntryInfo(id) { const modal = document.getElementById('infoModal'); const body = document.getElementById('infoModalBody'); const title = document.getElementById('infoModalTitle'); const subtitle = document.getElementById('infoModalSubtitle'); modal.classList.add('active'); body.innerHTML = '
'; try { const result = await api('get', { id: id }); if (result.success) { const entry = result.data; title.textContent = entry.ip_prefix; subtitle.textContent = entry.client_short_name ? `Client: ${entry.client_short_name}` : ''; const formatDate = (dateStr) => { if (!dateStr) return '-'; const d = new Date(dateStr); return d.toLocaleString(); }; const flagIcon = (value) => { return value ? '' : ''; }; const infoRow = (label, value, isCode = false) => { const displayVal = value || '-'; return ` ${label} ${isCode ? `${escapeHtml(value || '-')}` : displayVal} `; }; body.innerHTML = `

Basic Information

${infoRow('IP Prefix', entry.ip_prefix, true)} ${infoRow('Country', entry.country_code ? `${getFlagEmoji(entry.country_code)} ${entry.country_code}` : null)} ${infoRow('Region', entry.region_code)} ${infoRow('City', entry.city)} ${infoRow('Postal Code', entry.postal_code)} ${infoRow('Client', entry.client_short_name)} ${infoRow('Notes', entry.notes)}

IP Enrichment Data ${entry.ipr_enriched_at ? ` (Last updated: ${formatDate(entry.ipr_enriched_at)})` : ''}

${!entry.ipr_enriched_at ? `

Not enriched yet. Click the globe icon to enrich this entry.

` : ` ${infoRow('Hostname', entry.ipr_hostname, true)} ${infoRow('ISP', entry.ipr_isp)} ${infoRow('Organization', entry.ipr_org)} ${infoRow('ASN', entry.ipr_asn)} ${infoRow('ASN Domain', entry.ipr_asn_name)} ${infoRow('Connection Type', entry.ipr_connection_type)} ${infoRow('Country (IPR)', entry.ipr_country_name)} ${infoRow('Region (IPR)', entry.ipr_region_name)} ${infoRow('Timezone', entry.ipr_timezone)} ${infoRow('Coordinates', entry.ipr_latitude && entry.ipr_longitude ? `${entry.ipr_latitude}, ${entry.ipr_longitude}` : null)}
`}

Security Flags

${!entry.ipr_enriched_at ? `

Security flags will be available after enrichment.

` : `
${flagIcon(entry.flag_vpn)} VPN
${flagIcon(entry.flag_proxy)} Proxy
${flagIcon(entry.flag_tor)} Tor
${flagIcon(entry.flag_tor_exit)} Tor Exit
${flagIcon(entry.flag_relay)} Relay
${flagIcon(entry.flag_cloud_provider)} Cloud Provider
${flagIcon(entry.flag_abuser)} Abuser
${flagIcon(entry.flag_attacker)} Attacker
${flagIcon(entry.flag_bogon)} Bogon
${flagIcon(entry.flag_anonymous)} Anonymous
${flagIcon(entry.flag_threat)} Threat
`}

Route53 A Records

Loading...

Record Info

${infoRow('Created', formatDate(entry.created_at))} ${infoRow('Updated', formatDate(entry.updated_at))} ${infoRow('Enriched', formatDate(entry.ipr_enriched_at))} ${infoRow('Record ID', entry.id)}
`; // Load Route53 records for this prefix loadRoute53RecordsForInfo(entry.ip_prefix); } else { body.innerHTML = `
Failed to load entry: ${escapeHtml(result.error)}
`; } } catch (error) { body.innerHTML = `
Failed to load entry details
`; } } // Load Route53 records for info modal async function loadRoute53RecordsForInfo(ipPrefix) { const container = document.getElementById('infoRoute53Content'); if (!container) return; try { const result = await api('route53_records_by_prefix', { ip_prefix: ipPrefix }); if (result.success && result.records && result.records.length > 0) { container.innerHTML = `
${result.records.map(rec => ` `).join('')}
Hostname IP Address PTR Status
${escapeHtml(rec.hostname)} ${escapeHtml(rec.ip_address)} ${rec.ptr_status || 'unknown'}
${result.count} record${result.count !== 1 ? 's' : ''} found in Route53 for this prefix
`; } else { container.innerHTML = `

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 = `
${icon}
${escapeHtml(message)} `; 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 = '
'; try { const result = await api('system_info'); if (result.success) { const data = result.data; 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 Statistics

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()}
`; } else { container.innerHTML = `
Failed to load system info: ${escapeHtml(result.error)}
`; } } catch (error) { container.innerHTML = `
Failed to load system info
`; } } // Schema sync variables let pendingSchemaUpdates = null; // Check for schema updates async function checkSchemaUpdates() { const resultDiv = document.getElementById('schemaCheckResult'); const applyBtn = document.getElementById('applySchemaBtn'); resultDiv.style.display = 'block'; resultDiv.innerHTML = '
'; applyBtn.disabled = true; try { const result = await api('schema_check'); if (result.success) { pendingSchemaUpdates = result; if (!result.has_updates) { resultDiv.innerHTML = `
Database schema is up to date. No changes needed.
`; applyBtn.disabled = true; } else { let html = `
Schema updates available. Review the changes below and click "Apply Schema Updates" to apply them.
`; if (result.missing_tables.length > 0) { html += `

Missing Tables (${result.missing_tables.length})

${result.missing_tables.map(t => ``).join('')}
Table Name
${escapeHtml(t)}
`; } if (result.missing_columns.length > 0) { html += `

Missing Columns (${result.missing_columns.length})

${result.missing_columns.map(c => ` `).join('')}
Table Column Definition
${escapeHtml(c.table)} ${escapeHtml(c.column)} ${escapeHtml(c.definition)}
`; } if (result.missing_indexes.length > 0) { html += `

Missing Indexes (${result.missing_indexes.length})

${result.missing_indexes.map(i => ` `).join('')}
Table Index Name Columns
${escapeHtml(i.table)} ${escapeHtml(i.index)} ${escapeHtml(i.columns)}
`; } resultDiv.innerHTML = html; applyBtn.disabled = false; } } else { resultDiv.innerHTML = `
Failed to check schema: ${escapeHtml(result.error)}
`; } } catch (error) { resultDiv.innerHTML = `
Failed to check schema: ${error.message}
`; } } // Apply schema updates async function applySchemaUpdates() { if (!pendingSchemaUpdates || !pendingSchemaUpdates.has_updates) { showToast('No updates to apply', 'error'); return; } if (!confirm('Are you sure you want to apply the schema updates? This will modify your database structure.')) { return; } const resultDiv = document.getElementById('schemaCheckResult'); const applyBtn = document.getElementById('applySchemaBtn'); applyBtn.disabled = true; resultDiv.innerHTML = '
'; try { const result = await api('schema_apply', {}, 'POST', {}); if (result.success) { let html = ''; if (result.applied.length > 0) { html += `
${escapeHtml(result.message)}

Applied Changes (${result.applied.length})

${result.applied.map(a => ``).join('')}
${escapeHtml(a)}
`; } if (result.failed.length > 0) { html += `

Failed (${result.failed.length})

${result.failed.map(f => ``).join('')}
${escapeHtml(f)}
`; } if (result.applied.length === 0 && result.failed.length === 0) { html = `
No updates needed - schema is already up to date.
`; } resultDiv.innerHTML = html; pendingSchemaUpdates = null; showToast(result.message, 'success'); // Refresh system info loadSystemInfo(); } else { resultDiv.innerHTML = `
Failed to apply schema: ${escapeHtml(result.error)}
`; applyBtn.disabled = false; } } catch (error) { resultDiv.innerHTML = `
Failed to apply schema: ${error.message}
`; applyBtn.disabled = false; } } // Logout function async function logout() { try { await api('logout', {}); } catch (e) {} window.location.href = 'login.php'; } // Load error logs async function loadErrorLogs() { const container = document.getElementById('errorLogsContent'); const lines = document.getElementById('errorLogLines').value; container.innerHTML = '
'; try { const result = await api('error_logs', { lines: lines }); if (result.success) { if (result.lines.length === 0) { container.innerHTML = `
${result.message || 'No error log entries found.'}

Log path: ${escapeHtml(result.log_path)}

`; } else { const getTypeColor = (type) => { switch(type) { case 'fatal': return 'var(--error)'; case 'error': return 'var(--error)'; case 'warning': return 'var(--warning)'; case 'notice': return '#17a2b8'; case 'deprecated': return '#6c757d'; default: return 'var(--text-secondary)'; } }; const getTypeBadge = (type) => { const color = getTypeColor(type); return `${type}`; }; container.innerHTML = `
Showing ${result.total_lines} entries from ${escapeHtml(result.log_path)} (${escapeHtml(result.log_size_formatted)})
${result.lines.map(entry => ` `).join('')}
Type Timestamp Message
${getTypeBadge(entry.type)} ${entry.timestamp ? escapeHtml(entry.timestamp) : '-'} ${escapeHtml(entry.message)}
`; } } else { container.innerHTML = `
Failed to load error logs: ${escapeHtml(result.error)}
`; } } catch (error) { container.innerHTML = `
Failed to load error logs: ${error.message}
`; } } // ============================================= // AWS Route53 / PTR Record Functions // ============================================= // Load AWS settings async function loadAwsSettings() { try { const result = await api('aws_settings_get'); if (result.success && result.data) { document.getElementById('awsAccessKeyId').value = result.data.aws_access_key_id || ''; document.getElementById('awsSecretAccessKey').value = result.data.aws_secret_access_key || ''; document.getElementById('awsRegion').value = result.data.aws_region || 'us-east-1'; document.getElementById('awsHostedZones').value = result.data.aws_hosted_zones || ''; } } catch (error) { console.error('Failed to load AWS settings:', error); } } // Save AWS settings async function saveAwsSettings() { const data = { aws_access_key_id: document.getElementById('awsAccessKeyId').value, aws_secret_access_key: document.getElementById('awsSecretAccessKey').value, aws_region: document.getElementById('awsRegion').value, aws_hosted_zones: document.getElementById('awsHostedZones').value }; try { const result = await api('aws_settings_save', {}, 'POST', data); if (result.success) { showToast('AWS settings saved successfully', 'success'); } else { showToast(result.error || 'Failed to save AWS settings', 'error'); } } catch (error) { showToast('Failed to save AWS settings: ' + error.message, 'error'); } } // Load whitelabel settings async function loadWhitelabelSettings() { try { const result = await api('whitelabel_get'); if (result.success && result.settings) { document.getElementById('whitelabelAppName').value = result.settings.app_name || ''; document.getElementById('whitelabelCompanyName').value = result.settings.company_name || ''; document.getElementById('whitelabelIconUrl').value = result.settings.icon_url || ''; document.getElementById('whitelabelFaviconUrl').value = result.settings.favicon_url || ''; document.getElementById('whitelabelDefaultImportUrl').value = result.settings.default_import_url || ''; updateWhitelabelPreview(); applyWhitelabelSettings(result.settings); } } catch (error) { console.error('Failed to load whitelabel settings:', error); } } // Save whitelabel settings async function saveWhitelabelSettings() { const data = { app_name: document.getElementById('whitelabelAppName').value, company_name: document.getElementById('whitelabelCompanyName').value, icon_url: document.getElementById('whitelabelIconUrl').value, favicon_url: document.getElementById('whitelabelFaviconUrl').value, default_import_url: document.getElementById('whitelabelDefaultImportUrl').value }; try { const result = await api('whitelabel_save', {}, 'POST', data); if (result.success) { showToast('Whitelabel settings saved successfully', 'success'); applyWhitelabelSettings(data); } else { showToast(result.error || 'Failed to save whitelabel settings', 'error'); } } catch (error) { showToast('Failed to save whitelabel settings: ' + error.message, 'error'); } } // Update the preview in the whitelabel tab function updateWhitelabelPreview() { const appName = document.getElementById('whitelabelAppName').value || 'ISP IP Manager'; const companyName = document.getElementById('whitelabelCompanyName').value || ''; const iconUrl = document.getElementById('whitelabelIconUrl').value; document.getElementById('whitelabelPreviewAppName').textContent = appName; document.getElementById('whitelabelPreviewCompanyName').textContent = companyName; const previewIcon = document.getElementById('whitelabelPreviewIcon'); if (iconUrl) { previewIcon.innerHTML = ``; } else { previewIcon.innerHTML = ``; } } // Apply whitelabel settings to the page function applyWhitelabelSettings(settings) { // Update page title and header if (settings.app_name) { document.title = settings.app_name; document.querySelector('.logo-title').textContent = settings.app_name; } // Update company name subtitle const logoSubtitle = document.querySelector('.logo-subtitle'); if (logoSubtitle) { logoSubtitle.textContent = settings.company_name || ''; } // Update favicon if (settings.favicon_url) { let link = document.querySelector("link[rel*='icon']") || document.createElement('link'); link.type = 'image/x-icon'; link.rel = 'shortcut icon'; link.href = settings.favicon_url; document.getElementsByTagName('head')[0].appendChild(link); } // Update header icon if (settings.icon_url) { const logoIcon = document.querySelector('.logo-icon'); if (logoIcon) { logoIcon.innerHTML = ``; } } } // Add event listeners for preview updates document.addEventListener('DOMContentLoaded', function() { const appNameInput = document.getElementById('whitelabelAppName'); const companyNameInput = document.getElementById('whitelabelCompanyName'); const iconUrlInput = document.getElementById('whitelabelIconUrl'); if (appNameInput) appNameInput.addEventListener('input', updateWhitelabelPreview); if (companyNameInput) companyNameInput.addEventListener('input', updateWhitelabelPreview); if (iconUrlInput) iconUrlInput.addEventListener('input', updateWhitelabelPreview); }); // Test AWS connection async function testAwsConnection() { const resultDiv = document.getElementById('awsTestResult'); resultDiv.style.display = 'block'; resultDiv.innerHTML = '
Testing connection...
'; try { const result = await api('aws_test'); if (result.success) { resultDiv.innerHTML = `
Connection successful! Found ${result.zones_count} hosted zone(s).
`; } else { resultDiv.innerHTML = `
${escapeHtml(result.error || 'Connection failed')}
`; } } catch (error) { resultDiv.innerHTML = `
Connection failed: ${escapeHtml(error.message)}
`; } } // Load PTR zones into dropdown async function loadPtrZones() { const select = document.getElementById('ptrZoneSelect'); const notConfigured = document.getElementById('ptrNotConfigured'); const configured = document.getElementById('ptrConfigured'); const refreshAwsBtn = document.getElementById('refreshAwsBtn'); try { const result = await api('aws_zones'); if (result.success && result.zones && result.zones.length > 0) { notConfigured.style.display = 'none'; configured.style.display = 'block'; select.innerHTML = ''; result.zones.forEach(zone => { const option = document.createElement('option'); option.value = zone.id; option.textContent = zone.name + ' (' + zone.id + ')'; select.appendChild(option); }); // Auto-select if only one zone is configured if (result.zones.length === 1) { select.value = result.zones[0].id; loadCachedPtrRecords(); } } else { notConfigured.style.display = 'block'; configured.style.display = 'none'; } } catch (error) { notConfigured.style.display = 'block'; configured.style.display = 'none'; console.error('Failed to load PTR zones:', error); } } // Load cached PTR records from database async function loadCachedPtrRecords() { const zoneId = document.getElementById('ptrZoneSelect').value; const tbody = document.getElementById('ptrRecordsBody'); const countSpan = document.getElementById('ptrRecordCount'); const checkAllBtn = document.getElementById('checkAllPtrsBtn'); const refreshAwsBtn = document.getElementById('refreshAwsBtn'); const syncStatus = document.getElementById('ptrSyncStatus'); refreshAwsBtn.disabled = !zoneId; if (!zoneId) { tbody.innerHTML = ` Select a hosted zone to view A records `; countSpan.textContent = ''; checkAllBtn.disabled = true; syncStatus.style.display = 'none'; return; } tbody.innerHTML = `
`; try { const result = await api('ptr_cache_get', { zone_id: zoneId }); if (result.success) { const records = result.records || []; countSpan.textContent = records.length + ' record(s)'; checkAllBtn.disabled = records.length === 0; // Update sync status if (records.length > 0) { syncStatus.style.display = 'flex'; document.getElementById('lastAwsSync').textContent = result.last_sync ? formatDateTime(result.last_sync) : 'Never'; // Calculate stats let matches = 0, mismatches = 0, missing = 0, unknown = 0; let lastPtrCheck = null; records.forEach(r => { if (r.ptr_status === 'match') matches++; else if (r.ptr_status === 'mismatch') mismatches++; else if (r.ptr_status === 'missing') missing++; else unknown++; if (r.ptr_checked_at && (!lastPtrCheck || r.ptr_checked_at > lastPtrCheck)) { lastPtrCheck = r.ptr_checked_at; } }); document.getElementById('lastPtrCheck').textContent = lastPtrCheck ? formatDateTime(lastPtrCheck) : 'Never'; document.getElementById('statMatch').textContent = matches + ' Match'; document.getElementById('statMismatch').textContent = mismatches + ' Mismatch'; document.getElementById('statMissing').textContent = missing + ' Missing'; document.getElementById('statUnknown').textContent = unknown + ' Unchecked'; } else { syncStatus.style.display = 'none'; } if (records.length === 0) { tbody.innerHTML = ` No cached records. Click "Sync from AWS" to fetch A records. `; return; } // Store records globally for IPXO export window.cachedPtrRecords = records; document.getElementById('exportIpxoBtn').disabled = !records.some(r => r.ptr_status === 'mismatch' || r.ptr_status === 'missing'); tbody.innerHTML = records.map((record) => { const statusColor = getStatusColor(record.ptr_status); const statusLabel = (record.ptr_status || 'unknown').toUpperCase(); return ` ${escapeHtml(record.ip_address)} ${escapeHtml(record.hostname)} ${record.ttl || '-'} ${record.ptr_record || '-'} ${statusLabel} `}).join(''); } else { tbody.innerHTML = ` ${escapeHtml(result.error || 'Failed to load records')} `; countSpan.textContent = ''; checkAllBtn.disabled = true; syncStatus.style.display = 'none'; } } catch (error) { tbody.innerHTML = ` Failed to load records: ${escapeHtml(error.message)} `; countSpan.textContent = ''; syncStatus.style.display = 'none'; checkAllBtn.disabled = true; } } // Helper function to get status color function getStatusColor(status) { switch (status) { case 'match': return 'var(--success)'; case 'mismatch': return 'var(--warning)'; case 'missing': return 'var(--error)'; case 'error': return 'var(--error)'; default: return 'var(--text-tertiary)'; } } // Format date time for display function formatDateTime(dateStr) { if (!dateStr) return '-'; const date = new Date(dateStr); return date.toLocaleString(); } // Refresh from AWS - sync A records from Route53 async function refreshFromAws() { const zoneId = document.getElementById('ptrZoneSelect').value; if (!zoneId) { showToast('Please select a zone first', 'warning'); return; } const refreshAwsBtn = document.getElementById('refreshAwsBtn'); refreshAwsBtn.disabled = true; refreshAwsBtn.innerHTML = '
Syncing...'; try { const result = await api('ptr_cache_refresh', { zone_id: zoneId }); if (result.success) { showToast(`AWS sync complete: ${result.message}`, 'success'); await loadCachedPtrRecords(); } else { showToast(result.error || 'Failed to sync from AWS', 'error'); } } catch (error) { showToast('Failed to sync from AWS: ' + error.message, 'error'); } refreshAwsBtn.disabled = false; refreshAwsBtn.innerHTML = ` Sync from AWS `; } // Check single PTR record (for cached records) async function checkSinglePtr(recordId, button) { const row = button.closest('tr'); const ptrValueCell = row.querySelector('.ptr-value'); const ptrStatusCell = row.querySelector('.ptr-status'); const ip = row.getAttribute('data-ip'); button.disabled = true; button.innerHTML = '
'; try { const result = await api('ptr_lookup', { ip: ip }); if (result.success) { const ptr = result.ptr || ''; ptrValueCell.textContent = ptr || '(none)'; ptrValueCell.style.color = ptr ? 'var(--text-primary)' : 'var(--text-tertiary)'; const status = result.status || 'unknown'; const statusColor = getStatusColor(status); const statusLabel = status.toUpperCase(); ptrStatusCell.innerHTML = `${statusLabel}`; } else { ptrValueCell.textContent = 'Error'; ptrValueCell.style.color = 'var(--error)'; ptrStatusCell.innerHTML = `ERROR`; } } catch (error) { ptrValueCell.textContent = 'Error'; ptrValueCell.style.color = 'var(--error)'; ptrStatusCell.innerHTML = `ERROR`; } button.disabled = false; button.innerHTML = ` Check `; } // Check all PTR records (batch via API) async function checkAllPtrs() { const zoneId = document.getElementById('ptrZoneSelect').value; if (!zoneId) { showToast('Please select a zone first', 'warning'); return; } const checkAllBtn = document.getElementById('checkAllPtrsBtn'); checkAllBtn.disabled = true; checkAllBtn.innerHTML = '
Checking...'; try { const result = await api('ptr_check_all', { zone_id: zoneId }); if (result.success) { showToast(`Checked ${result.checked} PTR(s): ${result.matches} match, ${result.mismatches} mismatch, ${result.missing} missing`, 'success'); await loadCachedPtrRecords(); } else { showToast(result.error || 'Failed to check PTRs', 'error'); } } catch (error) { showToast('Failed to check PTRs: ' + error.message, 'error'); } checkAllBtn.disabled = false; checkAllBtn.innerHTML = ` Check All PTRs `; } // Edit A Record modal functions function editARecord(id, hostname, ip, ttl) { document.getElementById('editARecordId').value = id; document.getElementById('editARecordOldHostname').value = hostname; document.getElementById('editARecordHostname').value = hostname; document.getElementById('editARecordIp').value = ip; document.getElementById('editARecordTtl').value = ttl || 3600; document.getElementById('editARecordModal').classList.add('active'); } function closeEditARecordModal() { document.getElementById('editARecordModal').classList.remove('active'); } async function saveARecord(event) { event.preventDefault(); const zoneId = document.getElementById('ptrZoneSelect').value; if (!zoneId) { showToast('No zone selected', 'error'); return; } const id = document.getElementById('editARecordId').value; const oldHostname = document.getElementById('editARecordOldHostname').value; const hostname = document.getElementById('editARecordHostname').value; const ip = document.getElementById('editARecordIp').value; const ttl = parseInt(document.getElementById('editARecordTtl').value) || 3600; const saveBtn = document.getElementById('saveARecordBtn'); saveBtn.disabled = true; saveBtn.innerHTML = '
Updating...'; try { const result = await api('aws_update_a_record', {}, 'POST', { zone_id: zoneId, old_hostname: oldHostname, hostname: hostname, ip: ip, ttl: ttl }); if (result.success) { showToast('A record updated successfully', 'success'); closeEditARecordModal(); // Refresh the records from AWS await refreshFromAws(); } else { showToast(result.error || 'Failed to update A record', 'error'); } } catch (error) { showToast('Failed to update A record: ' + error.message, 'error'); } saveBtn.disabled = false; saveBtn.innerHTML = ` Update A Record `; } // Export IPXO JSON for mismatched/missing PTRs function exportIpxoJson() { const records = window.cachedPtrRecords || []; const mismatchedOrMissing = records.filter(r => r.ptr_status === 'mismatch' || r.ptr_status === 'missing'); if (mismatchedOrMissing.length === 0) { showToast('No mismatched or missing PTR records to export', 'warning'); return; } // Group records by /24 subnet const subnets = {}; mismatchedOrMissing.forEach(record => { const ip = record.ip_address; const parts = ip.split('.'); if (parts.length === 4) { const subnet = `${parts[0]}.${parts[1]}.${parts[2]}.0/24`; if (!subnets[subnet]) { subnets[subnet] = []; } subnets[subnet].push({ address: ip, dname: record.hostname, ttl: record.ttl || 3600 }); } }); // Download a separate JSON file for each /24 subnet const subnetKeys = Object.keys(subnets).sort(); let downloadCount = 0; subnetKeys.forEach((subnet, index) => { const ipxoData = { subnets: [{ prefix: subnet, records: subnets[subnet] }] }; const jsonStr = JSON.stringify(ipxoData, null, 2); const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); // Create filename from subnet (replace / and . for valid filename) const filename = `ipxo-ptr-${subnet.replace(/\//g, '-').replace(/\./g, '_')}.json`; // Stagger downloads slightly to avoid browser blocking setTimeout(() => { const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); downloadCount++; if (downloadCount === subnetKeys.length) { showToast(`Downloaded ${subnetKeys.length} IPXO JSON file(s) for ${mismatchedOrMissing.length} PTR record(s)`, 'success'); } }, index * 100); }); } // Clear error logs async function clearErrorLogs() { if (!confirm('Are you sure you want to clear the error log? This action cannot be undone.')) { return; } try { const result = await api('error_logs_clear', {}, 'POST', {}); if (result.success) { showToast(result.message, 'success'); loadErrorLogs(); } else { showToast(result.error || 'Failed to clear error logs', 'error'); } } catch (error) { showToast('Failed to clear error logs: ' + error.message, 'error'); } } // Close modals on escape key document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeModal(); closeDeleteModal(); closeClearAllModal(); closeInfoModal(); } }); // Close modals on overlay click document.querySelectorAll('.modal-overlay').forEach(overlay => { overlay.addEventListener('click', (e) => { if (e.target === overlay) { closeModal(); closeDeleteModal(); closeClearAllModal(); closeInfoModal(); } }); }); // Prevent zoom on iOS document.addEventListener('gesturestart', function(e) { e.preventDefault(); });