Files

507 lines
28 KiB
Twig
Raw Permalink Normal View History

{% extends 'layout/base.twig' %}
{% set title = 'TLD Import Logs' %}
{% set pageTitle = 'TLD Import Logs' %}
{% set pageDescription = 'History of TLD registry import operations' %}
{% set pageIcon = 'fas fa-history' %}
{% block content %}
{# Header with Actions #}
<div class="mb-4 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Import Logs</h1>
<p class="text-gray-600 dark:text-slate-400 mt-1">History of TLD registry import operations</p>
</div>
<div class="flex gap-2">
<a href="/tld-registry" class="inline-flex items-center px-4 py-2.5 border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-slate-300 text-sm rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors font-medium">
<i class="fas fa-arrow-left mr-2"></i>
Back to Registry
</a>
</div>
</div>
{# Statistics Cards #}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{# Total Imports Card #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wide">Total Imports</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-white mt-1">{{ importStats.total_imports|default(0) }}</p>
</div>
<div class="w-12 h-12 bg-blue-50 dark:bg-blue-500/10 rounded-lg flex items-center justify-center">
<i class="fas fa-download text-blue-600 dark:text-blue-400 text-lg"></i>
</div>
</div>
</div>
{# Successful Imports Card #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wide">Successful</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-white mt-1">{{ importStats.successful_imports|default(0) }}</p>
</div>
<div class="w-12 h-12 bg-green-50 dark:bg-green-500/10 rounded-lg flex items-center justify-center">
<i class="fas fa-check-circle text-green-600 dark:text-green-400 text-lg"></i>
</div>
</div>
</div>
{# Failed Imports Card #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wide">Failed</p>
<p class="text-2xl font-semibold text-gray-900 dark:text-white mt-1">{{ importStats.failed_imports|default(0) }}</p>
</div>
<div class="w-12 h-12 bg-red-50 dark:bg-red-500/10 rounded-lg flex items-center justify-center">
<i class="fas fa-times-circle text-red-600 dark:text-red-400 text-lg"></i>
</div>
</div>
</div>
{# Last Import Card #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 p-5 hover:shadow-md transition-shadow duration-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wide">Last Import</p>
<p class="text-sm font-semibold text-gray-900 dark:text-white mt-1">
{% if importStats.last_import is not empty %}
{{ importStats.last_import|date('M j, H:i') }}
{% else %}
Never
{% endif %}
</p>
</div>
<div class="w-12 h-12 bg-indigo-50 dark:bg-indigo-500/10 rounded-lg flex items-center justify-center">
<i class="fas fa-clock text-indigo-600 dark:text-indigo-400 text-lg"></i>
</div>
</div>
</div>
</div>
{# Import Logs Table #}
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
{% if imports is not empty %}
{# Table View (Desktop) #}
<div class="hidden lg:block overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-slate-700">
<thead class="bg-gray-50 dark:bg-slate-900">
<tr>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Import Type</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Results</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Publication Date</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Started</th>
<th class="px-6 py-3 text-right text-xs font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-slate-700">
{% set typeIcons = {
tld_list: 'fa-list',
rdap: 'fa-database',
whois: 'fa-server',
complete_workflow: 'fa-tasks',
check_updates: 'fa-sync-alt',
manual: 'fa-hand-pointer'
} %}
{% set typeLabels = {
tld_list: 'TLD List',
rdap: 'RDAP Servers',
whois: 'WHOIS Data',
complete_workflow: 'Complete Workflow',
check_updates: 'Update Check',
manual: 'Manual Import'
} %}
{% set typeDescriptions = {
tld_list: 'IANA TLD list import',
rdap: 'RDAP server bootstrap data',
whois: 'WHOIS server & registry URLs',
complete_workflow: 'Full import (TLD List → RDAP → WHOIS)',
check_updates: 'IANA update verification',
manual: 'Manual data import'
} %}
{% for import in imports %}
{% set icon = typeIcons[import.import_type]|default('fa-file-import') %}
{% set label = typeLabels[import.import_type]|default(import.import_type|capitalize) %}
{% set description = typeDescriptions[import.import_type]|default('Import operation') %}
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-150"
data-import-id="{{ import.id }}"
data-import-data="{{ import|json_encode|e('html_attr') }}">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-10 w-10 bg-primary bg-opacity-10 rounded-lg flex items-center justify-center">
<i class="fas {{ icon }} text-primary"></i>
</div>
<div class="ml-4">
<div class="text-sm font-semibold text-gray-900 dark:text-white">{{ label }}</div>
<div class="text-sm text-gray-500 dark:text-slate-400">{{ description }}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{% if import.status == 'completed' %}
{% set statusClass = 'bg-green-100 dark:bg-green-500/10 text-green-700 dark:text-green-400 border-green-200 dark:border-green-500/30' %}
{% set statusIcon = 'fa-check-circle' %}
{% set statusText = 'Completed' %}
{% elseif import.status == 'failed' %}
{% set statusClass = 'bg-red-100 dark:bg-red-500/10 text-red-700 dark:text-red-400 border-red-200 dark:border-red-500/30' %}
{% set statusIcon = 'fa-times-circle' %}
{% set statusText = 'Failed' %}
{% else %}
{% set statusClass = 'bg-yellow-100 dark:bg-yellow-500/10 text-yellow-700 dark:text-yellow-400 border-yellow-200 dark:border-yellow-500/30' %}
{% set statusIcon = 'fa-clock' %}
{% set statusText = 'In Progress' %}
{% endif %}
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border {{ statusClass }}">
<i class="fas {{ statusIcon }} mr-1"></i>
{{ statusText }}
</span>
</td>
<td class="px-6 py-4">
<div class="text-sm text-gray-900 dark:text-white">
<div class="flex items-center space-x-4">
<span class="flex items-center">
<i class="fas fa-globe text-gray-400 dark:text-slate-500 mr-1"></i>
{{ import.total_tlds }} total
</span>
<span class="flex items-center text-green-600 dark:text-green-400">
<i class="fas fa-plus mr-1"></i>
{{ import.new_tlds }} new
</span>
<span class="flex items-center text-blue-600 dark:text-blue-400">
<i class="fas fa-sync mr-1"></i>
{{ import.updated_tlds }} updated
</span>
{% if import.failed_tlds > 0 %}
<span class="flex items-center text-red-600 dark:text-red-400">
<i class="fas fa-exclamation-triangle mr-1"></i>
{{ import.failed_tlds }} failed
</span>
{% endif %}
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-slate-400">
{% if import.iana_publication_date %}
<div class="flex items-center">
<i class="far fa-calendar mr-2"></i>
{{ import.iana_publication_date|date('M j, Y') }}
</div>
{% else %}
<span class="text-gray-400 dark:text-slate-500">N/A</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-slate-400">
<div class="flex items-center">
<i class="far fa-clock mr-2"></i>
{{ import.started_at|date('M j, H:i') }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button onclick="showImportDetails({{ import.id }})" class="text-primary hover:text-primary-dark">
<i class="fas fa-eye"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# Card View (Mobile) #}
<div class="lg:hidden divide-y divide-gray-200 dark:divide-slate-700">
{% for import in imports %}
{% set icon = typeIcons[import.import_type]|default('fa-file-import') %}
{% set label = typeLabels[import.import_type]|default(import.import_type|capitalize) %}
<div class="p-6 hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors duration-150"
data-import-id="{{ import.id }}"
data-import-data="{{ import|json_encode|e('html_attr') }}">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<div class="w-10 h-10 bg-primary bg-opacity-10 rounded-lg flex items-center justify-center mr-3">
<i class="fas {{ icon }} text-primary"></i>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-white">{{ label }}</h3>
<p class="text-sm text-gray-500 dark:text-slate-400">{{ import.started_at|date('M j, Y H:i') }}</p>
</div>
</div>
{% if import.status == 'completed' %}
{% set mobileStatusClass = 'bg-green-100 dark:bg-green-500/10 text-green-700 dark:text-green-400' %}
{% set mobileStatusIcon = 'fa-check-circle' %}
{% set mobileStatusText = 'Completed' %}
{% elseif import.status == 'failed' %}
{% set mobileStatusClass = 'bg-red-100 dark:bg-red-500/10 text-red-700 dark:text-red-400' %}
{% set mobileStatusIcon = 'fa-times-circle' %}
{% set mobileStatusText = 'Failed' %}
{% else %}
{% set mobileStatusClass = 'bg-yellow-100 dark:bg-yellow-500/10 text-yellow-700 dark:text-yellow-400' %}
{% set mobileStatusIcon = 'fa-clock' %}
{% set mobileStatusText = 'In Progress' %}
{% endif %}
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold {{ mobileStatusClass }}">
<i class="fas {{ mobileStatusIcon }} mr-1"></i>
{{ mobileStatusText }}
</span>
</div>
<div class="space-y-2 text-sm">
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-slate-400">Total TLDs:</span>
<span class="font-semibold text-gray-900 dark:text-white">{{ import.total_tlds }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-slate-400">New:</span>
<span class="font-semibold text-green-600 dark:text-green-400">{{ import.new_tlds }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-slate-400">Updated:</span>
<span class="font-semibold text-blue-600 dark:text-blue-400">{{ import.updated_tlds }}</span>
</div>
{% if import.failed_tlds > 0 %}
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-slate-400">Failed:</span>
<span class="font-semibold text-red-600 dark:text-red-400">{{ import.failed_tlds }}</span>
</div>
{% endif %}
</div>
<div class="flex space-x-2 mt-3">
<button onclick="showImportDetails({{ import.id }})" class="flex-1 px-3 py-1.5 bg-blue-50 dark:bg-blue-500/10 text-blue-600 dark:text-blue-400 rounded text-center text-sm hover:bg-blue-100 dark:hover:bg-blue-500/20 transition-colors">
<i class="fas fa-eye mr-1"></i> Details
</button>
</div>
</div>
{% endfor %}
</div>
{# Pagination #}
{% if pagination.total_pages > 1 %}
<div class="bg-white dark:bg-slate-800 px-4 py-3 flex items-center justify-between border-t border-gray-200 dark:border-slate-700 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
{% if pagination.current_page > 1 %}
<a href="?page={{ pagination.current_page - 1 }}"
class="relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-slate-600 text-sm font-medium rounded-md text-gray-700 dark:text-slate-300 bg-white dark:bg-slate-800 hover:bg-gray-50 dark:hover:bg-slate-700">
Previous
</a>
{% endif %}
{% if pagination.current_page < pagination.total_pages %}
<a href="?page={{ pagination.current_page + 1 }}"
class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-slate-600 text-sm font-medium rounded-md text-gray-700 dark:text-slate-300 bg-white dark:bg-slate-800 hover:bg-gray-50 dark:hover:bg-slate-700">
Next
</a>
{% endif %}
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700 dark:text-slate-300">
Showing <span class="font-medium">{{ pagination.showing_from }}</span> to
<span class="font-medium">{{ pagination.showing_to }}</span> of
<span class="font-medium">{{ pagination.total }}</span> results
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
{% for i in 1..pagination.total_pages %}
<a href="?page={{ i }}"
class="relative inline-flex items-center px-4 py-2 border text-sm font-medium {{ i == pagination.current_page ? 'z-10 bg-primary border-primary text-white' : 'bg-white dark:bg-slate-800 border-gray-300 dark:border-slate-600 text-gray-500 dark:text-slate-400 hover:bg-gray-50 dark:hover:bg-slate-700' }} {{ i == 1 ? 'rounded-l-md' : '' }} {{ i == pagination.total_pages ? 'rounded-r-md' : '' }}">
{{ i }}
</a>
{% endfor %}
</nav>
</div>
</div>
</div>
{% endif %}
{% else %}
<div class="text-center py-12 px-6">
<div class="mb-4">
<i class="fas fa-history text-gray-300 dark:text-slate-600 text-6xl"></i>
</div>
<h3 class="text-lg font-semibold text-gray-700 dark:text-slate-300 mb-1">No Import Logs</h3>
<p class="text-sm text-gray-500 dark:text-slate-400 mb-4">No TLD imports have been performed yet.</p>
<a href="/tld-registry" class="inline-flex items-center px-5 py-2.5 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
<i class="fas fa-arrow-left mr-2"></i>
Back to Registry
</a>
</div>
{% endif %}
</div>
{# Import Details Modal #}
<div id="importDetailsModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
<div class="relative top-20 mx-auto p-5 border border-gray-200 dark:border-slate-700 w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white dark:bg-slate-800">
<div class="mt-3">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Import Details</h3>
<button onclick="closeImportDetails()" class="text-gray-400 dark:text-slate-500 hover:text-gray-600 dark:hover:text-slate-300">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<div id="importDetailsContent" class="text-sm text-gray-600 dark:text-slate-400">
</div>
</div>
</div>
</div>
<script>
function showImportDetails(importId) {
const importData = findImportData(importId);
if (!importData) {
document.getElementById('importDetailsContent').innerHTML = `
<div class="text-center text-gray-500 dark:text-slate-400">
<p>Import details not found</p>
</div>
`;
document.getElementById('importDetailsModal').classList.remove('hidden');
return;
}
const typeLabels = {
'tld_list': 'TLD List',
'rdap': 'RDAP Servers',
'whois': 'WHOIS Data',
'complete_workflow': 'Complete Workflow',
'check_updates': 'Update Check',
'manual': 'Manual Import'
};
const typeDescriptions = {
'tld_list': 'IANA TLD list import',
'rdap': 'RDAP server bootstrap data',
'whois': 'WHOIS server & registry URLs',
'complete_workflow': 'Full import (TLD List → RDAP → WHOIS)',
'check_updates': 'IANA update verification',
'manual': 'Manual data import'
};
const typeLabel = typeLabels[importData.import_type] || importData.import_type;
const typeDescription = typeDescriptions[importData.import_type] || 'Import operation';
let duration = 'Unknown';
if (importData.started_at && importData.completed_at) {
const start = new Date(importData.started_at);
const end = new Date(importData.completed_at);
const diffMs = end - start;
const minutes = Math.floor(diffMs / 60000);
const seconds = Math.floor((diffMs % 60000) / 1000);
if (diffMs < 5000 && importData.import_type === 'complete_workflow') {
const estimatedSeconds = Math.round((importData.total_tlds || 0) * 1.1);
const estMinutes = Math.floor(estimatedSeconds / 60);
const estSeconds = estimatedSeconds % 60;
duration = `~${estMinutes} minutes ${estSeconds} seconds (estimated)`;
} else if (minutes === 0 && seconds === 0) {
duration = 'Less than 1 second';
} else {
duration = `${minutes} minutes ${seconds} seconds`;
}
}
let statusClass = 'bg-gray-100 dark:bg-slate-700 text-gray-800 dark:text-slate-200';
let statusText = 'Unknown';
if (importData.status === 'completed') {
statusClass = 'bg-green-100 dark:bg-green-500/10 text-green-800 dark:text-green-400';
statusText = 'Completed';
} else if (importData.status === 'failed') {
statusClass = 'bg-red-100 dark:bg-red-500/10 text-red-800 dark:text-red-400';
statusText = 'Failed';
} else if (importData.status === 'running') {
statusClass = 'bg-yellow-100 dark:bg-yellow-500/10 text-yellow-800 dark:text-yellow-400';
statusText = 'Running';
}
document.getElementById('importDetailsContent').innerHTML = `
<div class="space-y-3">
<div class="flex justify-between">
<span class="font-medium text-gray-900 dark:text-white">Import ID:</span>
<span class="text-gray-700 dark:text-slate-300">${importData.id}</span>
</div>
<div class="flex justify-between">
<span class="font-medium text-gray-900 dark:text-white">Type:</span>
<span class="text-gray-700 dark:text-slate-300">${typeLabel}</span>
</div>
<div class="flex justify-between">
<span class="font-medium text-gray-900 dark:text-white">Description:</span>
<span class="text-gray-600 dark:text-slate-400">${typeDescription}</span>
</div>
<div class="flex justify-between">
<span class="font-medium text-gray-900 dark:text-white">Status:</span>
<span class="px-2 py-1 rounded text-xs ${statusClass}">${statusText}</span>
</div>
<div class="flex justify-between">
<span class="font-medium text-gray-900 dark:text-white">Duration:</span>
<span class="text-gray-700 dark:text-slate-300">${duration}</span>
</div>
<div class="flex justify-between">
<span class="font-medium text-gray-900 dark:text-white">Started:</span>
<span class="text-gray-700 dark:text-slate-300">${new Date(importData.started_at).toLocaleString()}</span>
</div>
${importData.completed_at ? `
<div class="flex justify-between">
<span class="font-medium text-gray-900 dark:text-white">Completed:</span>
<span class="text-gray-700 dark:text-slate-300">${new Date(importData.completed_at).toLocaleString()}</span>
</div>
` : ''}
${importData.iana_publication_date ? `
<div class="flex justify-between">
<span class="font-medium text-gray-900 dark:text-white">IANA Publication:</span>
<span class="text-gray-700 dark:text-slate-300">${importData.iana_publication_date}</span>
</div>
` : ''}
<div class="mt-4">
<h4 class="font-medium text-gray-900 dark:text-white mb-2">Import Results:</h4>
<div class="bg-gray-100 dark:bg-slate-900 p-3 rounded text-xs font-mono space-y-1 text-gray-800 dark:text-slate-300">
<div>Total TLDs: ${importData.total_tlds || 0}</div>
<div>New TLDs: ${importData.new_tlds || 0}</div>
<div>Updated TLDs: ${importData.updated_tlds || 0}</div>
<div>Failed TLDs: ${importData.failed_tlds || 0}</div>
${importData.error_message ? `
<div class="text-red-600 dark:text-red-400 mt-2">
<strong>Error:</strong> ${importData.error_message}
</div>
` : ''}
</div>
</div>
</div>
`;
document.getElementById('importDetailsModal').classList.remove('hidden');
}
function findImportData(importId) {
const importRows = document.querySelectorAll('tr[data-import-id]');
for (let row of importRows) {
if (row.getAttribute('data-import-id') == importId) {
return JSON.parse(row.getAttribute('data-import-data'));
}
}
const importCards = document.querySelectorAll('[data-import-id]');
for (let card of importCards) {
if (card.getAttribute('data-import-id') == importId) {
return JSON.parse(card.getAttribute('data-import-data'));
}
}
return null;
}
function closeImportDetails() {
document.getElementById('importDetailsModal').classList.add('hidden');
}
document.getElementById('importDetailsModal').addEventListener('click', function(e) {
if (e.target === this) {
closeImportDetails();
}
});
</script>
{% endblock %}