Add pagination to all data tables using jQuery DataTables

Libraries Added:
- jQuery 3.7.1 from CDN
- DataTables 1.13.7 (CSS + JS) from CDN

Custom Styling:
- Integrated DataTables styling with existing design
- Custom pagination button styles
- Responsive search and filter inputs

Paginated Tables:
- jobsTable: Crawl jobs (25/page, sorted by ID desc)
- pagesTable: Crawled pages (50/page)
- linksTable: Found links (50/page)
- brokenTable: Broken links (25/page)
- redirectsTable: Redirects (25/page)
- seoTable: SEO issues (25/page)

Features:
- Search functionality per table
- Column sorting
- Configurable entries per page
- German localization
- Automatic reinitialization on data reload
- Navigation controls (First/Previous/Next/Last)
- Entry count display

All quality checks pass:
- PHPStan Level 8: 0 errors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-04 09:49:39 +02:00
parent c40d44e4c9
commit 1588f83624

View File

@@ -13,6 +13,16 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Crawler</title> <title>Web Crawler</title>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<!-- DataTables CSS -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.7/css/jquery.dataTables.min.css">
<!-- DataTables JS -->
<script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
<style> <style>
* { * {
margin: 0; margin: 0;
@@ -207,6 +217,58 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
/* DataTables Styling */
.dataTables_wrapper {
padding: 20px 0;
}
.dataTables_filter input {
padding: 8px;
border: 2px solid #e0e0e0;
border-radius: 6px;
margin-left: 10px;
}
.dataTables_length select {
padding: 6px;
border: 2px solid #e0e0e0;
border-radius: 6px;
margin: 0 10px;
}
.dataTables_info {
padding-top: 10px;
color: #7f8c8d;
}
.dataTables_paginate {
padding-top: 10px;
}
.dataTables_paginate .paginate_button {
padding: 6px 12px;
margin: 0 2px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background: white;
cursor: pointer;
}
.dataTables_paginate .paginate_button.current {
background: #3498db;
color: white !important;
border-color: #3498db;
}
.dataTables_paginate .paginate_button:hover {
background: #ecf0f1;
}
.dataTables_paginate .paginate_button.disabled {
cursor: not-allowed;
opacity: 0.5;
}
</style> </style>
</head> </head>
<body> <body>
@@ -223,7 +285,7 @@
<div class="card"> <div class="card">
<h2>Crawl Jobs</h2> <h2>Crawl Jobs</h2>
<table id="jobsTable"> <table id="jobsTable" class="display">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
@@ -256,7 +318,7 @@
</div> </div>
<div class="tab-content active" id="pages-tab"> <div class="tab-content active" id="pages-tab">
<table> <table id="pagesTable" class="display">
<thead> <thead>
<tr> <tr>
<th>URL</th> <th>URL</th>
@@ -272,7 +334,7 @@
</div> </div>
<div class="tab-content" id="links-tab"> <div class="tab-content" id="links-tab">
<table> <table id="linksTable" class="display">
<thead> <thead>
<tr> <tr>
<th>Von</th> <th>Von</th>
@@ -289,7 +351,7 @@
</div> </div>
<div class="tab-content" id="broken-tab"> <div class="tab-content" id="broken-tab">
<table> <table id="brokenTable" class="display">
<thead> <thead>
<tr> <tr>
<th>URL</th> <th>URL</th>
@@ -307,7 +369,7 @@
<div class="tab-content" id="redirects-tab"> <div class="tab-content" id="redirects-tab">
<h3>Redirect Statistics</h3> <h3>Redirect Statistics</h3>
<div id="redirectStats" class="stats" style="margin-bottom: 20px;"></div> <div id="redirectStats" class="stats" style="margin-bottom: 20px;"></div>
<table> <table id="redirectsTable" class="display">
<thead> <thead>
<tr> <tr>
<th>URL</th> <th>URL</th>
@@ -326,7 +388,7 @@
<div class="tab-content" id="seo-tab"> <div class="tab-content" id="seo-tab">
<h3>SEO Issues</h3> <h3>SEO Issues</h3>
<div id="seoStats" style="margin-bottom: 20px;"></div> <div id="seoStats" style="margin-bottom: 20px;"></div>
<table> <table id="seoTable" class="display">
<thead> <thead>
<tr> <tr>
<th>URL</th> <th>URL</th>
@@ -380,12 +442,19 @@
} }
} }
let jobsDataTable = null;
async function loadJobs() { async function loadJobs() {
try { try {
const response = await fetch('/api.php?action=jobs'); const response = await fetch('/api.php?action=jobs');
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
// Destroy existing DataTable if it exists
if (jobsDataTable) {
jobsDataTable.destroy();
}
const tbody = document.getElementById('jobsBody'); const tbody = document.getElementById('jobsBody');
tbody.innerHTML = data.jobs.map(job => ` tbody.innerHTML = data.jobs.map(job => `
<tr> <tr>
@@ -402,6 +471,25 @@
</td> </td>
</tr> </tr>
`).join(''); `).join('');
// Initialize DataTable
jobsDataTable = $('#jobsTable').DataTable({
pageLength: 25,
order: [[0, 'desc']],
language: {
search: 'Suchen:',
lengthMenu: 'Zeige _MENU_ Einträge',
info: 'Zeige _START_ bis _END_ von _TOTAL_ Einträgen',
infoEmpty: 'Keine Einträge verfügbar',
infoFiltered: '(gefiltert von _MAX_ Einträgen)',
paginate: {
first: 'Erste',
last: 'Letzte',
next: 'Nächste',
previous: 'Vorherige'
}
}
});
} }
} catch (e) { } catch (e) {
console.error('Fehler beim Laden der Jobs:', e); console.error('Fehler beim Laden der Jobs:', e);
@@ -473,6 +561,10 @@
const pagesResponse = await fetch(`/api.php?action=pages&job_id=${currentJobId}`); const pagesResponse = await fetch(`/api.php?action=pages&job_id=${currentJobId}`);
const pagesData = await pagesResponse.json(); const pagesData = await pagesResponse.json();
if ($.fn.DataTable.isDataTable('#pagesTable')) {
$('#pagesTable').DataTable().destroy();
}
if (pagesData.success && pagesData.pages.length > 0) { if (pagesData.success && pagesData.pages.length > 0) {
document.getElementById('pagesBody').innerHTML = pagesData.pages.map(page => ` document.getElementById('pagesBody').innerHTML = pagesData.pages.map(page => `
<tr> <tr>
@@ -482,12 +574,33 @@
<td>${page.crawled_at}</td> <td>${page.crawled_at}</td>
</tr> </tr>
`).join(''); `).join('');
$('#pagesTable').DataTable({
pageLength: 50,
language: {
search: 'Suchen:',
lengthMenu: 'Zeige _MENU_ Einträge',
info: 'Zeige _START_ bis _END_ von _TOTAL_ Einträgen',
infoEmpty: 'Keine Einträge verfügbar',
infoFiltered: '(gefiltert von _MAX_ Einträgen)',
paginate: {
first: 'Erste',
last: 'Letzte',
next: 'Nächste',
previous: 'Vorherige'
}
}
});
} }
// Load links // Load links
const linksResponse = await fetch(`/api.php?action=links&job_id=${currentJobId}`); const linksResponse = await fetch(`/api.php?action=links&job_id=${currentJobId}`);
const linksData = await linksResponse.json(); const linksData = await linksResponse.json();
if ($.fn.DataTable.isDataTable('#linksTable')) {
$('#linksTable').DataTable().destroy();
}
if (linksData.success && linksData.links.length > 0) { if (linksData.success && linksData.links.length > 0) {
document.getElementById('linksBody').innerHTML = linksData.links.map(link => ` document.getElementById('linksBody').innerHTML = linksData.links.map(link => `
<tr> <tr>
@@ -498,12 +611,33 @@
<td>${link.is_internal ? 'Intern' : '<span class="external">Extern</span>'}</td> <td>${link.is_internal ? 'Intern' : '<span class="external">Extern</span>'}</td>
</tr> </tr>
`).join(''); `).join('');
$('#linksTable').DataTable({
pageLength: 50,
language: {
search: 'Suchen:',
lengthMenu: 'Zeige _MENU_ Einträge',
info: 'Zeige _START_ bis _END_ von _TOTAL_ Einträgen',
infoEmpty: 'Keine Einträge verfügbar',
infoFiltered: '(gefiltert von _MAX_ Einträgen)',
paginate: {
first: 'Erste',
last: 'Letzte',
next: 'Nächste',
previous: 'Vorherige'
}
}
});
} }
// Load broken links // Load broken links
const brokenResponse = await fetch(`/api.php?action=broken-links&job_id=${currentJobId}`); const brokenResponse = await fetch(`/api.php?action=broken-links&job_id=${currentJobId}`);
const brokenData = await brokenResponse.json(); const brokenData = await brokenResponse.json();
if ($.fn.DataTable.isDataTable('#brokenTable')) {
$('#brokenTable').DataTable().destroy();
}
if (brokenData.success && brokenData.broken_links.length > 0) { if (brokenData.success && brokenData.broken_links.length > 0) {
document.getElementById('brokenBody').innerHTML = brokenData.broken_links.map(page => ` document.getElementById('brokenBody').innerHTML = brokenData.broken_links.map(page => `
<tr> <tr>
@@ -513,6 +647,23 @@
<td>${page.crawled_at}</td> <td>${page.crawled_at}</td>
</tr> </tr>
`).join(''); `).join('');
$('#brokenTable').DataTable({
pageLength: 25,
language: {
search: 'Suchen:',
lengthMenu: 'Zeige _MENU_ Einträge',
info: 'Zeige _START_ bis _END_ von _TOTAL_ Einträgen',
infoEmpty: 'Keine Einträge verfügbar',
infoFiltered: '(gefiltert von _MAX_ Einträgen)',
paginate: {
first: 'Erste',
last: 'Letzte',
next: 'Nächste',
previous: 'Vorherige'
}
}
});
} else { } else {
document.getElementById('brokenBody').innerHTML = '<tr><td colspan="4" class="loading">Keine defekten Links gefunden</td></tr>'; document.getElementById('brokenBody').innerHTML = '<tr><td colspan="4" class="loading">Keine defekten Links gefunden</td></tr>';
} }
@@ -539,6 +690,10 @@
`; `;
// SEO Issues // SEO Issues
if ($.fn.DataTable.isDataTable('#seoTable')) {
$('#seoTable').DataTable().destroy();
}
if (seoData.issues.length > 0) { if (seoData.issues.length > 0) {
document.getElementById('seoIssuesBody').innerHTML = seoData.issues.map(item => ` document.getElementById('seoIssuesBody').innerHTML = seoData.issues.map(item => `
<tr> <tr>
@@ -548,6 +703,23 @@
<td><span class="nofollow">${item.issues.join(', ')}</span></td> <td><span class="nofollow">${item.issues.join(', ')}</span></td>
</tr> </tr>
`).join(''); `).join('');
$('#seoTable').DataTable({
pageLength: 25,
language: {
search: 'Suchen:',
lengthMenu: 'Zeige _MENU_ Einträge',
info: 'Zeige _START_ bis _END_ von _TOTAL_ Einträgen',
infoEmpty: 'Keine Einträge verfügbar',
infoFiltered: '(gefiltert von _MAX_ Einträgen)',
paginate: {
first: 'Erste',
last: 'Letzte',
next: 'Nächste',
previous: 'Vorherige'
}
}
});
} else { } else {
document.getElementById('seoIssuesBody').innerHTML = '<tr><td colspan="4" class="loading">Keine SEO-Probleme gefunden</td></tr>'; document.getElementById('seoIssuesBody').innerHTML = '<tr><td colspan="4" class="loading">Keine SEO-Probleme gefunden</td></tr>';
} }
@@ -598,6 +770,10 @@
`; `;
// Redirect Table // Redirect Table
if ($.fn.DataTable.isDataTable('#redirectsTable')) {
$('#redirectsTable').DataTable().destroy();
}
if (redirectsData.redirects.length > 0) { if (redirectsData.redirects.length > 0) {
document.getElementById('redirectsBody').innerHTML = redirectsData.redirects.map(redirect => { document.getElementById('redirectsBody').innerHTML = redirectsData.redirects.map(redirect => {
const isExcessive = redirect.redirect_count > stats.threshold; const isExcessive = redirect.redirect_count > stats.threshold;
@@ -614,6 +790,23 @@
</tr> </tr>
`; `;
}).join(''); }).join('');
$('#redirectsTable').DataTable({
pageLength: 25,
language: {
search: 'Suchen:',
lengthMenu: 'Zeige _MENU_ Einträge',
info: 'Zeige _START_ bis _END_ von _TOTAL_ Einträgen',
infoEmpty: 'Keine Einträge verfügbar',
infoFiltered: '(gefiltert von _MAX_ Einträgen)',
paginate: {
first: 'Erste',
last: 'Letzte',
next: 'Nächste',
previous: 'Vorherige'
}
}
});
} else { } else {
document.getElementById('redirectsBody').innerHTML = '<tr><td colspan="5" class="loading">Keine Redirects gefunden</td></tr>'; document.getElementById('redirectsBody').innerHTML = '<tr><td colspan="5" class="loading">Keine Redirects gefunden</td></tr>';
} }