add stuff

This commit is contained in:
Purple
2026-01-16 20:56:25 +00:00
parent e874f976dc
commit 4fa73c488f
2 changed files with 67 additions and 66 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -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);