feat: Add progress bar for batch SEO audit
- Progress section with bar and percentage - Real-time log showing each company being processed - Score display with color indicators (🟢🟡🔴) - Cancel button to stop audit mid-process - Summary at the end with success/failed/skipped counts - 500ms delay between requests to avoid API overload Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0d2b26031d
commit
a916b297c7
@ -462,6 +462,80 @@
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* Progress Section */
|
||||
.progress-section {
|
||||
background: white;
|
||||
padding: var(--spacing-lg);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.progress-title {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
height: 24px;
|
||||
background: var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: var(--primary);
|
||||
border-radius: 12px;
|
||||
transition: width 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.progress-message {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.progress-log {
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
font-size: var(--font-size-sm);
|
||||
background: var(--background);
|
||||
padding: var(--spacing-md);
|
||||
border-radius: var(--radius);
|
||||
margin-top: var(--spacing-md);
|
||||
}
|
||||
|
||||
.progress-log-entry {
|
||||
padding: var(--spacing-xs) 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.progress-log-entry.success { color: var(--success); font-weight: 600; }
|
||||
.progress-log-entry.error { color: var(--error); font-weight: 600; }
|
||||
.progress-log-entry.skipped { color: var(--warning, #f59e0b); }
|
||||
.progress-log-entry.detail { color: var(--text-secondary); font-size: 0.9em; padding-left: 1em; border-bottom: none; }
|
||||
.progress-log-entry.info { color: var(--text-secondary); font-style: italic; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@ -519,6 +593,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Section (hidden by default) -->
|
||||
<div class="progress-section" id="progressSection">
|
||||
<div class="progress-header">
|
||||
<span class="progress-title">Audyt SEO w toku...</span>
|
||||
<button class="btn btn-sm btn-outline" onclick="cancelAudit()">Anuluj</button>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar-fill" id="progressBar" style="width: 0%">0%</div>
|
||||
</div>
|
||||
<div class="progress-message" id="progressMessage">Przygotowywanie...</div>
|
||||
<div class="progress-log" id="progressLog"></div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filters-bar">
|
||||
<div class="filter-group">
|
||||
@ -597,7 +684,11 @@
|
||||
</thead>
|
||||
<tbody id="seoTableBody">
|
||||
{% for company in companies %}
|
||||
<tr data-category="{{ company.category }}"
|
||||
<tr data-company-id="{{ company.id }}"
|
||||
data-slug="{{ company.slug }}"
|
||||
data-company-name="{{ company.name }}"
|
||||
data-website="{{ company.website or '' }}"
|
||||
data-category="{{ company.category }}"
|
||||
data-name="{{ company.name|lower }}"
|
||||
data-overall="{{ company.seo_score if company.seo_score is not none else -1 }}"
|
||||
data-performance="{{ company.performance_score if company.performance_score is not none else -1 }}"
|
||||
@ -877,11 +968,44 @@ function resetFilters() {
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
let auditInProgress = false;
|
||||
|
||||
function addLogEntry(logElement, message, type) {
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'progress-log-entry ' + type;
|
||||
entry.textContent = message;
|
||||
logElement.appendChild(entry);
|
||||
logElement.scrollTop = logElement.scrollHeight;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function getScoreClass(score) {
|
||||
if (score >= 90) return '🟢';
|
||||
if (score >= 50) return '🟡';
|
||||
return '🔴';
|
||||
}
|
||||
|
||||
function cancelAudit() {
|
||||
auditInProgress = false;
|
||||
document.getElementById('progressSection').classList.remove('active');
|
||||
document.getElementById('batchAuditBtn').disabled = false;
|
||||
document.getElementById('batchAuditBtn').innerHTML = `
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
Uruchom audyt
|
||||
`;
|
||||
}
|
||||
|
||||
// Audit functions
|
||||
function runSingleAudit(slug) {
|
||||
showModal(
|
||||
'Uruchom audyt SEO',
|
||||
'Czy na pewno chcesz uruchomić audyt SEO dla tej firmy? Analiza może potrwać kilka minut.',
|
||||
'Czy na pewno chcesz uruchomić audyt SEO dla tej firmy? Analiza może potrwać kilka sekund.',
|
||||
async () => {
|
||||
try {
|
||||
const response = await fetch('/api/seo/audit', {
|
||||
@ -908,22 +1032,138 @@ function runSingleAudit(slug) {
|
||||
);
|
||||
}
|
||||
|
||||
function runBatchAudit() {
|
||||
async function runBatchAudit() {
|
||||
showModal(
|
||||
'Uruchom audyt wsadowy',
|
||||
'Czy na pewno chcesz uruchomić audyt SEO dla wszystkich firm? To może potrwać dłuższy czas.',
|
||||
() => {
|
||||
'Uruchom audyt wszystkich firm',
|
||||
'Czy chcesz uruchomić audyt SEO dla wszystkich firm z adresem WWW? Każda firma będzie analizowana osobno.',
|
||||
async () => {
|
||||
auditInProgress = true;
|
||||
const btn = document.getElementById('batchAuditBtn');
|
||||
const originalText = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span>Audyt w toku...</span>';
|
||||
|
||||
showInfoModal('Audyt uruchomiony', 'Audyt wsadowy został uruchomiony w tle. Może to potrwać kilkadziesiąt minut. Sprawdź wyniki później.');
|
||||
const progressSection = document.getElementById('progressSection');
|
||||
progressSection.classList.add('active');
|
||||
|
||||
setTimeout(() => {
|
||||
const progressBar = document.getElementById('progressBar');
|
||||
const progressMessage = document.getElementById('progressMessage');
|
||||
const progressLog = document.getElementById('progressLog');
|
||||
|
||||
progressBar.style.width = '0%';
|
||||
progressBar.textContent = '0%';
|
||||
progressMessage.textContent = 'Pobieranie listy firm...';
|
||||
progressLog.innerHTML = '';
|
||||
|
||||
// Get all companies with website from the table
|
||||
const rows = document.querySelectorAll('#seoTableBody tr[data-slug]');
|
||||
const companies = [];
|
||||
rows.forEach(row => {
|
||||
const website = row.dataset.website;
|
||||
if (website && website.trim() !== '') {
|
||||
companies.push({
|
||||
slug: row.dataset.slug,
|
||||
name: row.dataset.companyName || row.dataset.name,
|
||||
website: website
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (companies.length === 0) {
|
||||
progressSection.classList.remove('active');
|
||||
showInfoModal('Brak firm', 'Nie znaleziono firm z adresem WWW do audytu.');
|
||||
auditInProgress = false;
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = originalText;
|
||||
}, 2000);
|
||||
btn.innerHTML = `
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
Uruchom audyt
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
addLogEntry(progressLog, `Znaleziono ${companies.length} firm z adresem WWW`, 'info');
|
||||
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (let i = 0; i < companies.length; i++) {
|
||||
if (!auditInProgress) {
|
||||
addLogEntry(progressLog, 'Audyt anulowany przez użytkownika', 'error');
|
||||
break;
|
||||
}
|
||||
|
||||
const company = companies[i];
|
||||
const percent = Math.round(((i + 1) / companies.length) * 100);
|
||||
|
||||
progressBar.style.width = `${percent}%`;
|
||||
progressBar.textContent = `${percent}%`;
|
||||
progressMessage.textContent = `[${i + 1}/${companies.length}] Analizuję: ${company.name}...`;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/seo/audit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ slug: company.slug })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
success++;
|
||||
const scores = data.data || {};
|
||||
const seo = scores.seo_score ?? '-';
|
||||
const perf = scores.performance_score ?? '-';
|
||||
const acc = scores.accessibility_score ?? '-';
|
||||
const bp = scores.best_practices_score ?? '-';
|
||||
|
||||
addLogEntry(progressLog, `✓ ${company.name}`, 'success');
|
||||
addLogEntry(progressLog, ` SEO: ${seo !== '-' ? getScoreClass(seo) + ' ' + seo : '-'} | Perf: ${perf !== '-' ? getScoreClass(perf) + ' ' + perf : '-'} | Dostępność: ${acc !== '-' ? getScoreClass(acc) + ' ' + acc : '-'} | BP: ${bp !== '-' ? getScoreClass(bp) + ' ' + bp : '-'}`, 'detail');
|
||||
} else {
|
||||
if (data.error && data.error.includes('Brak strony WWW')) {
|
||||
skipped++;
|
||||
addLogEntry(progressLog, `⊘ ${company.name} - Brak WWW`, 'skipped');
|
||||
} else {
|
||||
failed++;
|
||||
addLogEntry(progressLog, `✗ ${company.name} - ${data.error || 'Błąd'}`, 'error');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
failed++;
|
||||
addLogEntry(progressLog, `✗ ${company.name} - Błąd połączenia`, 'error');
|
||||
}
|
||||
|
||||
// Delay between requests to avoid overwhelming the API
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
progressBar.style.width = '100%';
|
||||
progressBar.textContent = '100%';
|
||||
progressMessage.textContent = `Audyt zakończony! Sukces: ${success}, Błędy: ${failed}, Pominięte: ${skipped}`;
|
||||
|
||||
addLogEntry(progressLog, `─────────────────────────────`, 'info');
|
||||
addLogEntry(progressLog, `PODSUMOWANIE: Sukces: ${success}, Błędy: ${failed}, Pominięte: ${skipped}`, 'info');
|
||||
|
||||
showInfoModal(
|
||||
'Audyt zakończony',
|
||||
`Przetworzono ${companies.length} firm.\n\nSukces: ${success}\nBłędy: ${failed}\nPominięte: ${skipped}`
|
||||
);
|
||||
|
||||
auditInProgress = false;
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = `
|
||||
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
Uruchom audyt
|
||||
`;
|
||||
|
||||
// Refresh page after a delay
|
||||
setTimeout(() => location.reload(), 3000);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user