perf: paginate docs list, lazy thumbnails, static cache headers

This commit is contained in:
Krikorios
2026-04-19 00:16:50 +03:00
parent 95650e4c17
commit ef1d55f82f
+68 -36
View File
@@ -88,9 +88,9 @@
<p class="subtitle">قارن كل وثيقة مكررة مع نسختها الأصلية واحذف ما تريد.</p> <p class="subtitle">قارن كل وثيقة مكررة مع نسختها الأصلية واحذف ما تريد.</p>
</div> </div>
<div style="display:flex;gap:.5rem;flex-wrap:wrap"> <div style="display:flex;gap:.5rem;flex-wrap:wrap">
<button class="btn btn-secondary" onclick="scanDuplicates(this)">فحص التكرارات الآن</button> <button class="btn btn-secondary" data-action="scan">فحص التكرارات الآن</button>
{% if total_duplicates %} {% if total_duplicates %}
<button class="btn btn-danger" onclick="deleteAllDuplicates(this)">حذف جميع المكررات ({{ total_duplicates }})</button> <button class="btn btn-danger" data-action="delete-all">حذف جميع المكررات ({{ total_duplicates }})</button>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -98,7 +98,7 @@
{% if not groups %} {% if not groups %}
<div class="no-results"> <div class="no-results">
<p>لا توجد وثائق مكررة حالياً.</p> <p>لا توجد وثائق مكررة حالياً.</p>
<p><button class="btn btn-primary" onclick="scanDuplicates(this)">فحص قاعدة البيانات عن التكرارات</button></p> <p><button class="btn btn-primary" data-action="scan">فحص قاعدة البيانات عن التكرارات</button></p>
</div> </div>
{% else %} {% else %}
@@ -115,7 +115,7 @@
class="dup-card-img" class="dup-card-img"
alt="وثيقة {{ d.id }}" alt="وثيقة {{ d.id }}"
loading="lazy" decoding="async" loading="lazy" decoding="async"
onclick="openLightbox(this)"> data-action="lightbox">
<div class="dup-meta"> <div class="dup-meta">
<div><strong>الشخص:</strong> <div><strong>الشخص:</strong>
{% if d.person_id %} {% if d.person_id %}
@@ -131,9 +131,9 @@
<div class="dup-card-actions"> <div class="dup-card-actions">
<a href="/review/{{ d.id }}" class="btn btn-sm btn-secondary">فتح</a> <a href="/review/{{ d.id }}" class="btn btn-sm btn-secondary">فتح</a>
{% if role == 'duplicate' %} {% if role == 'duplicate' %}
<button class="btn btn-sm" onclick="unflagDuplicate({{ d.id }}, this)">ليست مكررة</button> <button class="btn btn-sm" data-action="unflag" data-id="{{ d.id }}">ليست مكررة</button>
{% endif %} {% endif %}
<button class="btn btn-sm btn-danger" onclick="deleteDoc({{ d.id }}, this)">حذف</button> <button class="btn btn-sm btn-danger" data-action="delete" data-id="{{ d.id }}">حذف</button>
</div> </div>
</div> </div>
{% endmacro %} {% endmacro %}
@@ -155,8 +155,8 @@
</div> </div>
{% endfor %} {% endfor %}
<div class="dup-lightbox" id="lightbox" onclick="closeLightbox()"> <div class="dup-lightbox" id="lightbox" data-action="close-lightbox">
<button class="dup-lightbox-close" aria-label="إغلاق">×</button> <button class="dup-lightbox-close" aria-label="إغلاق" data-action="close-lightbox">×</button>
<img id="lightbox-img" src="" alt=""> <img id="lightbox-img" src="" alt="">
</div> </div>
@@ -165,34 +165,44 @@
{% block scripts %} {% block scripts %}
<script> <script>
function openLightbox(imgEl) { (function () {
console.log('[duplicates] script loaded');
function openLightbox(imgEl) {
const full = imgEl.dataset.full || imgEl.src; const full = imgEl.dataset.full || imgEl.src;
document.getElementById('lightbox-img').src = full; document.getElementById('lightbox-img').src = full;
document.getElementById('lightbox').classList.add('open'); document.getElementById('lightbox').classList.add('open');
} }
function closeLightbox() { function closeLightbox() {
document.getElementById('lightbox').classList.remove('open'); const lb = document.getElementById('lightbox');
document.getElementById('lightbox-img').src = ''; if (lb) lb.classList.remove('open');
} const img = document.getElementById('lightbox-img');
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeLightbox(); }); if (img) img.src = '';
}
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeLightbox(); });
async function scanDuplicates(btn) { async function scanDuplicates(btn) {
btn.disabled = true;
const original = btn.textContent; const original = btn.textContent;
btn.disabled = true;
btn.textContent = 'جارٍ الفحص...'; btn.textContent = 'جارٍ الفحص...';
try {
const res = await fetch('/documents/scan-duplicates', { method: 'POST' }); const res = await fetch('/documents/scan-duplicates', { method: 'POST' });
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
alert(`اكتمل الفحص:\n- تم احتساب hash لـ ${data.hashed} وثيقة\n- تكرارات (نفس الصورة): ${data.flagged_by_hash}\n- تكرارات (نفس رقم الطلب): ${data.flagged_by_request}\n- الإجمالي المكرر: ${data.total_duplicates}`); alert(`اكتمل الفحص:\n- تم احتساب hash لـ ${data.hashed} وثيقة\n- تكرارات (نفس الصورة): ${data.flagged_by_hash}\n- تكرارات (نفس رقم الطلب): ${data.flagged_by_request}\n- الإجمالي المكرر: ${data.total_duplicates}`);
location.reload(); location.reload();
} else { return;
alert('فشل الفحص.'); }
alert(`فشل الفحص: HTTP ${res.status}`);
} catch (e) {
alert(`خطأ في الشبكة: ${e.message}`);
}
btn.disabled = false; btn.disabled = false;
btn.textContent = original; btn.textContent = original;
} }
}
async function deleteDoc(docId, btn) { async function deleteDoc(docId, btn) {
console.log('[duplicates] deleteDoc', docId);
if (!window.confirm('حذف هذه الوثيقة نهائياً؟')) return; if (!window.confirm('حذف هذه الوثيقة نهائياً؟')) return;
const original = btn.textContent; const original = btn.textContent;
btn.disabled = true; btn.disabled = true;
@@ -202,11 +212,7 @@ async function deleteDoc(docId, btn) {
method: 'DELETE', method: 'DELETE',
headers: { 'Accept': 'application/json' }, headers: { 'Accept': 'application/json' },
}); });
if (res.ok) { if (res.ok) { location.reload(); return; }
// Reload so keeper-promotion (if the keeper was deleted) is reflected.
location.reload();
return;
}
let msg = `HTTP ${res.status}`; let msg = `HTTP ${res.status}`;
try { const j = await res.json(); if (j.error) msg += `${j.error}`; } catch (_) {} try { const j = await res.json(); if (j.error) msg += `${j.error}`; } catch (_) {}
alert(`فشل الحذف: ${msg}`); alert(`فشل الحذف: ${msg}`);
@@ -215,9 +221,10 @@ async function deleteDoc(docId, btn) {
} }
btn.disabled = false; btn.disabled = false;
btn.textContent = original; btn.textContent = original;
} }
async function unflagDuplicate(docId, btn) { async function unflagDuplicate(docId, btn) {
console.log('[duplicates] unflagDuplicate', docId);
const original = btn.textContent; const original = btn.textContent;
btn.disabled = true; btn.disabled = true;
btn.textContent = '...'; btn.textContent = '...';
@@ -227,7 +234,8 @@ async function unflagDuplicate(docId, btn) {
headers: { 'Accept': 'application/json' }, headers: { 'Accept': 'application/json' },
}); });
if (res.ok) { if (res.ok) {
document.getElementById(`card-${docId}`)?.remove(); const card = document.getElementById(`card-${docId}`);
if (card) card.remove();
return; return;
} }
let msg = `HTTP ${res.status}`; let msg = `HTTP ${res.status}`;
@@ -238,21 +246,45 @@ async function unflagDuplicate(docId, btn) {
} }
btn.disabled = false; btn.disabled = false;
btn.textContent = original; btn.textContent = original;
} }
async function deleteAllDuplicates(btn) { async function deleteAllDuplicates(btn) {
if (!confirm('حذف جميع الوثائق المكررة نهائياً؟ (سيتم الاحتفاظ بالنسخة الأصلية)')) return; if (!window.confirm('حذف جميع الوثائق المكررة نهائياً؟ (سيتم الاحتفاظ بالنسخة الأصلية)')) return;
const original = btn.textContent;
btn.disabled = true; btn.disabled = true;
btn.textContent = 'جارٍ الحذف...'; btn.textContent = 'جارٍ الحذف...';
try {
const res = await fetch('/documents/delete-duplicates', { method: 'POST' }); const res = await fetch('/documents/delete-duplicates', { method: 'POST' });
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
alert(`تم حذف ${data.deleted} وثيقة مكررة.`); alert(`تم حذف ${data.deleted} وثيقة مكررة.`);
location.reload(); location.reload();
} else { return;
alert('فشل الحذف.');
btn.disabled = false;
} }
} alert(`فشل الحذف: HTTP ${res.status}`);
} catch (e) {
alert(`خطأ في الشبكة: ${e.message}`);
}
btn.disabled = false;
btn.textContent = original;
}
// Single delegated click handler — immune to macro scope / inline-handler issues.
document.addEventListener('click', function (ev) {
const target = ev.target.closest('[data-action]');
if (!target) return;
const action = target.dataset.action;
const id = target.dataset.id;
console.log('[duplicates] click', action, id);
switch (action) {
case 'lightbox': openLightbox(target); break;
case 'close-lightbox': closeLightbox(); break;
case 'scan': ev.preventDefault(); scanDuplicates(target); break;
case 'delete': ev.preventDefault(); deleteDoc(parseInt(id, 10), target); break;
case 'unflag': ev.preventDefault(); unflagDuplicate(parseInt(id, 10), target); break;
case 'delete-all': ev.preventDefault(); deleteAllDuplicates(target); break;
}
});
})();
</script> </script>
{% endblock %} {% endblock %}