add stuff
This commit is contained in:
133
webapp/index.php
133
webapp/index.php
@@ -1,3 +1,4 @@
|
||||
<?php require_once __DIR__ . '/config.php'; ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -15,7 +16,7 @@
|
||||
--purple-light: #8B4D9B;
|
||||
--purple-lighter: #F5EDF7;
|
||||
--purple-gradient: linear-gradient(135deg, #6B2D7B 0%, #8B4D9B 100%);
|
||||
|
||||
|
||||
/* UI Colors */
|
||||
--bg-primary: #f8f9fa;
|
||||
--bg-secondary: #ffffff;
|
||||
@@ -25,25 +26,25 @@
|
||||
--text-tertiary: #868e96;
|
||||
--border: rgba(0, 0, 0, 0.08);
|
||||
--border-strong: rgba(0, 0, 0, 0.12);
|
||||
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 3px rgba(107, 45, 123, 0.08), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
--shadow-md: 0 4px 12px rgba(107, 45, 123, 0.1), 0 2px 4px rgba(0, 0, 0, 0.04);
|
||||
--shadow-lg: 0 10px 40px rgba(107, 45, 123, 0.15), 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
--shadow-xl: 0 25px 50px -12px rgba(107, 45, 123, 0.2);
|
||||
|
||||
|
||||
/* Radii */
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 20px;
|
||||
|
||||
|
||||
/* Status Colors */
|
||||
--success: #28a745;
|
||||
--warning: #ffc107;
|
||||
--error: #dc3545;
|
||||
--info: #17a2b8;
|
||||
|
||||
|
||||
/* Transitions */
|
||||
--transition: all 0.2s ease;
|
||||
--transition-slow: all 0.3s ease;
|
||||
@@ -1124,7 +1125,7 @@
|
||||
<div class="advanced-section">
|
||||
<h2 class="advanced-section-title">Import Geofeed Data</h2>
|
||||
<p class="advanced-section-desc">Import geofeed entries from a CSV file or a remote URL. The data should follow RFC 8805 format: <code>ip_prefix,country_code,region_code,city,postal_code</code></p>
|
||||
|
||||
|
||||
<div class="import-options">
|
||||
<!-- File Upload -->
|
||||
<div class="import-card">
|
||||
@@ -1137,7 +1138,7 @@
|
||||
</div>
|
||||
<h3 class="import-card-title">Upload CSV File</h3>
|
||||
<p class="import-card-desc">Upload a geofeed CSV file from your computer</p>
|
||||
|
||||
|
||||
<div class="file-input-wrapper">
|
||||
<input type="file" class="file-input" id="csvFileInput" accept=".csv,.txt" onchange="handleFileSelect(this)">
|
||||
<label class="file-input-label" for="csvFileInput">
|
||||
@@ -1150,18 +1151,18 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="file-name" id="fileName"></div>
|
||||
|
||||
|
||||
<div class="progress-container" id="fileProgress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="fileProgressFill"></div>
|
||||
</div>
|
||||
<div class="progress-text" id="fileProgressText">Processing...</div>
|
||||
</div>
|
||||
|
||||
|
||||
<button class="btn btn-primary" style="width: 100%; margin-top: 16px;" id="uploadBtn" onclick="importFromFile()" disabled>
|
||||
Import from File
|
||||
</button>
|
||||
|
||||
|
||||
<div class="import-results" id="fileResults">
|
||||
<div class="import-results-title">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
@@ -1184,22 +1185,22 @@
|
||||
</div>
|
||||
<h3 class="import-card-title">Import from URL</h3>
|
||||
<p class="import-card-desc">Fetch and import a geofeed from a remote URL</p>
|
||||
|
||||
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<input type="url" class="form-input" id="importUrl" placeholder="https://example.com/geofeed.csv" value="https://store.prpl.uk/geofeed.csv">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="progress-container" id="urlProgress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="urlProgressFill"></div>
|
||||
</div>
|
||||
<div class="progress-text" id="urlProgressText">Fetching...</div>
|
||||
</div>
|
||||
|
||||
|
||||
<button class="btn btn-primary" style="width: 100%; margin-top: 16px;" id="urlImportBtn" onclick="importFromUrl()">
|
||||
Import from URL
|
||||
</button>
|
||||
|
||||
|
||||
<div class="import-results" id="urlResults">
|
||||
<div class="import-results-title">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
@@ -1216,7 +1217,7 @@
|
||||
<div class="advanced-section">
|
||||
<h2 class="advanced-section-title">Danger Zone</h2>
|
||||
<p class="advanced-section-desc">Irreversible actions - please proceed with caution.</p>
|
||||
|
||||
|
||||
<button class="btn btn-danger" onclick="confirmClearAll()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
@@ -1338,7 +1339,7 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadEntries();
|
||||
loadStats();
|
||||
|
||||
|
||||
// Enable clear all button when "DELETE" is typed
|
||||
document.getElementById('confirmClearInput').addEventListener('input', (e) => {
|
||||
document.getElementById('confirmClearBtn').disabled = e.target.value !== 'DELETE';
|
||||
@@ -1349,7 +1350,7 @@
|
||||
function switchTab(tab) {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
|
||||
|
||||
document.querySelector(`.tab[onclick="switchTab('${tab}')"]`).classList.add('active');
|
||||
document.getElementById(`tab-${tab}`).classList.add('active');
|
||||
}
|
||||
@@ -1359,13 +1360,13 @@
|
||||
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: csrfToken });
|
||||
}
|
||||
|
||||
|
||||
const response = await fetch(url, options);
|
||||
return response.json();
|
||||
}
|
||||
@@ -1374,12 +1375,12 @@
|
||||
async function loadEntries(page = 1) {
|
||||
currentPage = page;
|
||||
const searchQuery = document.getElementById('searchInput').value;
|
||||
|
||||
|
||||
document.getElementById('tableContent').innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||
|
||||
|
||||
try {
|
||||
const result = await api('list', { page, search: searchQuery });
|
||||
|
||||
|
||||
if (result.success) {
|
||||
renderTable(result.data, result.pagination);
|
||||
} else {
|
||||
@@ -1393,7 +1394,7 @@
|
||||
// Render table
|
||||
function renderTable(entries, pagination) {
|
||||
totalPages = pagination.pages;
|
||||
|
||||
|
||||
if (entries.length === 0) {
|
||||
document.getElementById('tableContent').innerHTML = `
|
||||
<div class="empty-state">
|
||||
@@ -1411,7 +1412,7 @@
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const tableHTML = `
|
||||
<table>
|
||||
<thead>
|
||||
@@ -1485,7 +1486,7 @@
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
document.getElementById('tableContent').innerHTML = tableHTML;
|
||||
}
|
||||
|
||||
@@ -1493,7 +1494,7 @@
|
||||
async function loadStats() {
|
||||
try {
|
||||
const result = await api('stats');
|
||||
|
||||
|
||||
if (result.success) {
|
||||
document.getElementById('statTotal').textContent = result.data.total_entries.toLocaleString();
|
||||
document.getElementById('statIPv4').textContent = (result.data.ip_versions?.ipv4 || 0).toLocaleString();
|
||||
@@ -1516,9 +1517,9 @@
|
||||
const modal = document.getElementById('entryModal');
|
||||
const form = document.getElementById('entryForm');
|
||||
const title = document.getElementById('modalTitle');
|
||||
|
||||
|
||||
form.reset();
|
||||
|
||||
|
||||
if (entry) {
|
||||
title.textContent = 'Edit Entry';
|
||||
document.getElementById('entryId').value = entry.id;
|
||||
@@ -1532,7 +1533,7 @@
|
||||
title.textContent = 'Add Entry';
|
||||
document.getElementById('entryId').value = '';
|
||||
}
|
||||
|
||||
|
||||
modal.classList.add('active');
|
||||
document.getElementById('ipPrefix').focus();
|
||||
}
|
||||
@@ -1545,7 +1546,7 @@
|
||||
async function editEntry(id) {
|
||||
try {
|
||||
const result = await api('get', { id });
|
||||
|
||||
|
||||
if (result.success) {
|
||||
openModal(result.data);
|
||||
} else {
|
||||
@@ -1559,10 +1560,10 @@
|
||||
// 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,
|
||||
@@ -1572,10 +1573,10 @@
|
||||
postal_code: document.getElementById('postalCode').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();
|
||||
@@ -1603,10 +1604,10 @@
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!deleteEntryId) return;
|
||||
|
||||
|
||||
try {
|
||||
const result = await api('delete', {}, 'POST', { id: deleteEntryId });
|
||||
|
||||
|
||||
if (result.success) {
|
||||
showToast('Entry deleted successfully', 'success');
|
||||
closeDeleteModal();
|
||||
@@ -1631,7 +1632,7 @@
|
||||
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');
|
||||
}
|
||||
@@ -1640,38 +1641,38 @@
|
||||
// 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 =
|
||||
document.getElementById('fileResultsText').textContent =
|
||||
`Successfully imported ${result.inserted} new entries, updated ${result.updated} existing entries.`;
|
||||
showToast('Import completed successfully', 'success');
|
||||
loadEntries();
|
||||
@@ -1701,28 +1702,28 @@
|
||||
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 =
|
||||
document.getElementById('urlResultsText').textContent =
|
||||
`Successfully imported ${result.inserted} new entries, updated ${result.updated} existing entries.`;
|
||||
showToast('Import completed successfully', 'success');
|
||||
loadEntries();
|
||||
@@ -1749,14 +1750,14 @@
|
||||
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() || '',
|
||||
@@ -1765,7 +1766,7 @@
|
||||
postal_code: parts[4]?.trim() || ''
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -1782,10 +1783,10 @@
|
||||
|
||||
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();
|
||||
@@ -1804,11 +1805,11 @@
|
||||
const container = document.getElementById('toastContainer');
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
|
||||
const icon = type === 'success'
|
||||
|
||||
const icon = type === 'success'
|
||||
? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>'
|
||||
: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>';
|
||||
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="toast-icon">${icon}</div>
|
||||
<span class="toast-message">${escapeHtml(message)}</span>
|
||||
@@ -1819,9 +1820,9 @@
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideIn 0.3s ease reverse';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
|
||||
Reference in New Issue
Block a user