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

This commit is contained in:
Krikorios
2026-04-18 23:42:06 +03:00
parent 86ff96816a
commit 379359be20
9 changed files with 773 additions and 71 deletions
+96 -3
View File
@@ -18,6 +18,10 @@
<div class="success-banner">تم رفع الملفات بنجاح وبدأ الاستخراج. قد يستغرق بعض الوقت.</div>
{% endif %}
{% if duplicates_skipped %}
<div class="alert-banner">تم تجاهل {{ duplicates_skipped }} ملف مكرر (موجود مسبقاً في النظام).</div>
{% endif %}
<div class="stats-bar">
<a href="/documents" class="stat {{ 'active' if not current_status }}">
<span class="stat-num">{{ stats.total or 0 }}</span><span class="stat-label">الكل</span>
@@ -36,8 +40,24 @@
<span class="stat-num">{{ stats.errors }}</span><span class="stat-label">أخطاء</span>
</a>
{% endif %}
{% if stats.duplicates %}
<a href="/documents/duplicates" class="stat">
<span class="stat-num">{{ stats.duplicates }}</span><span class="stat-label">مكررة</span>
</a>
{% endif %}
</div>
<div style="display:flex;gap:.5rem;margin:.75rem 0;flex-wrap:wrap">
<a href="/documents/duplicates" class="btn btn-sm btn-secondary">عرض المكررات</a>
<button class="btn btn-sm btn-secondary" onclick="scanDuplicates(this)">فحص التكرارات في قاعدة البيانات</button>
</div>
{% if stats.errors and current_status=='error' %}
<div class="alert-banner">
<button class="btn btn-sm btn-primary" onclick="retryAllErrors(this)">إعادة معالجة جميع الأخطاء ({{ stats.errors }})</button>
</div>
{% endif %}
{% if stats.pending_review %}
<div class="alert-banner">
<a href="/review/next">ابدأ مراجعة {{ stats.pending_review }} وثيقة →</a>
@@ -63,7 +83,7 @@
<td data-label="#">{{ d.id }}</td>
<td data-label="الصورة">
<a href="/review/{{ d.id }}" title="فتح الوثيقة {{ d.id }} للمراجعة" aria-label="فتح الوثيقة {{ d.id }} للمراجعة">
<img src="/uploads/{{ d.image_path }}" class="doc-thumb-mini" alt="">
<img src="/thumbs/{{ d.image_path }}" class="doc-thumb-mini" alt="" loading="lazy" decoding="async" width="80" height="80">
</a>
</td>
<td data-label="الشخص">
@@ -72,11 +92,19 @@
{% else %}—{% endif %}
</td>
<td data-label="رقم الطلب">{{ d.request_number or '—' }}</td>
<td data-label="الحالة"><span class="status-badge status-{{ d.status }}">{{ d.status }}</span></td>
<td data-label="الحالة">
<span class="status-badge status-{{ d.status }}">{{ d.status }}</span>
{% if d.duplicate_of %}
<a href="/review/{{ d.duplicate_of }}" class="status-badge" style="background:#fde68a;color:#92400e;margin-inline-start:.25rem" title="مكرر للوثيقة رقم {{ d.duplicate_of }}">مكرر</a>
{% endif %}
</td>
<td data-label="التاريخ">{{ d.created_at[:10] if d.created_at else '' }}</td>
<td data-label="الإجراءات" class="doc-actions">
{% if d.status == 'error' %}
<button class="btn btn-sm btn-primary" onclick="retryDoc({{ d.id }}, this)" title="{{ d.extraction_error or '' }}">إعادة محاولة</button>
{% endif %}
<a href="/review/{{ d.id }}" class="btn btn-sm btn-secondary">
{% if d.status == 'confirmed' %}عرض{% else %}مراجعة{% endif %}
{% if d.status == 'confirmed' %}عرض{% elif d.status == 'error' %}مراجعة{% else %}مراجعة{% endif %}
</a>
<button class="btn btn-sm btn-danger" onclick="deleteDoc({{ d.id }}, this)">حذف</button>
</td>
@@ -91,6 +119,23 @@
<p>لا توجد وثائق. <a href="/">ارفع وثائق جديدة</a></p>
</div>
{% endif %}
{% if total_pages and total_pages > 1 %}
{% set qs = ('status=' ~ current_status ~ '&') if current_status else '' %}
<div class="pagination" style="display:flex;gap:.5rem;justify-content:center;align-items:center;margin:1rem 0;flex-wrap:wrap">
{% if page > 1 %}
<a class="btn btn-sm btn-secondary" href="?{{ qs }}page={{ page - 1 }}">السابق</a>
{% else %}
<span class="btn btn-sm btn-secondary" style="opacity:.5;pointer-events:none">السابق</span>
{% endif %}
<span style="font-size:.9rem">صفحة {{ page }} من {{ total_pages }} — {{ total_filtered }} وثيقة</span>
{% if page < total_pages %}
<a class="btn btn-sm btn-secondary" href="?{{ qs }}page={{ page + 1 }}">التالي</a>
{% else %}
<span class="btn btn-sm btn-secondary" style="opacity:.5;pointer-events:none">التالي</span>
{% endif %}
</div>
{% endif %}
{% endblock %}
{% block scripts %}
@@ -108,5 +153,53 @@ async function deleteDoc(docId, btn) {
btn.textContent = 'حذف';
}
}
async function retryDoc(docId, btn) {
btn.disabled = true;
btn.textContent = '...';
const res = await fetch(`/documents/${docId}/retry`, { method: 'POST' });
if (res.ok) {
location.reload();
} else {
alert('تعذّرت إعادة المحاولة.');
btn.disabled = false;
btn.textContent = 'إعادة محاولة';
}
}
async function retryAllErrors(btn) {
if (!confirm('إعادة معالجة جميع الوثائق التي بها أخطاء؟')) return;
btn.disabled = true;
const original = btn.textContent;
btn.textContent = 'جارٍ الإرسال...';
const res = await fetch('/documents/retry-errors', { method: 'POST' });
if (res.ok) {
location.href = '/documents?status=pending';
} else {
alert('فشلت إعادة المعالجة.');
btn.disabled = false;
btn.textContent = original;
}
}
async function scanDuplicates(btn) {
btn.disabled = true;
const original = btn.textContent;
btn.textContent = 'جارٍ الفحص...';
const res = await fetch('/documents/scan-duplicates', { method: 'POST' });
if (res.ok) {
const data = await res.json();
alert(`اكتمل الفحص:\n- تم احتساب hash لـ ${data.hashed} وثيقة\n- تكرارات (نفس الصورة): ${data.flagged_by_hash}\n- تكرارات (نفس رقم الطلب): ${data.flagged_by_request}\n- الإجمالي المكرر: ${data.total_duplicates}`);
if (data.total_duplicates > 0) {
location.href = '/documents/duplicates';
} else {
location.reload();
}
} else {
alert('فشل الفحص.');
btn.disabled = false;
btn.textContent = original;
}
}
</script>
{% endblock %}
+113
View File
@@ -0,0 +1,113 @@
{% extends "base.html" %}
{% block title %}الوثائق المكررة — سجل العقارات{% endblock %}
{% block content %}
<div class="page-header">
<div>
<span class="page-kicker">مراجعة التكرارات</span>
<h1>الوثائق المكررة</h1>
<p class="subtitle">وثائق تمّ رصدها كتكرار لوثيقة موجودة مسبقاً. يمكن حذفها بأمان.</p>
</div>
<div style="display:flex;gap:.5rem">
<button class="btn btn-secondary" onclick="scanDuplicates(this)">فحص التكرارات الآن</button>
{% if documents %}
<button class="btn btn-danger" onclick="deleteAllDuplicates(this)">حذف جميع التكرارات ({{ documents|length }})</button>
{% endif %}
</div>
</div>
{% if not documents %}
<div class="no-results">
<p>لا توجد وثائق مكررة حالياً.</p>
<p><button class="btn btn-primary" onclick="scanDuplicates(this)">فحص قاعدة البيانات عن التكرارات</button></p>
</div>
{% else %}
<div class="table-wrapper">
<table class="results-table responsive-table">
<thead>
<tr>
<th>#</th>
<th>الصورة</th>
<th>الشخص</th>
<th>رقم الطلب</th>
<th>مكرر للوثيقة</th>
<th>الحالة</th>
<th></th>
</tr>
</thead>
<tbody>
{% for d in documents %}
<tr id="row-{{ d.id }}">
<td data-label="#">{{ d.id }}</td>
<td data-label="الصورة">
<a href="/review/{{ d.id }}"><img src="/thumbs/{{ d.image_path }}" class="doc-thumb-mini" alt="" loading="lazy" decoding="async" width="80" height="80"></a>
</td>
<td data-label="الشخص">
{% if d.person_id %}
<a href="/persons/{{ d.person_id }}">{{ d.first_name or '' }} {{ d.family_name or '' }}</a>
{% else %}—{% endif %}
</td>
<td data-label="رقم الطلب">{{ d.request_number or '—' }}</td>
<td data-label="مكرر للوثيقة">
<a href="/review/{{ d.duplicate_of }}">#{{ d.duplicate_of }}</a>
{% if d.orig_created_at %}<br><small>{{ d.orig_created_at[:10] }}</small>{% endif %}
</td>
<td data-label="الحالة"><span class="status-badge status-{{ d.status }}">{{ d.status }}</span></td>
<td data-label="الإجراءات" class="doc-actions">
<a href="/review/{{ d.id }}" class="btn btn-sm btn-secondary">عرض</a>
<button class="btn btn-sm btn-danger" onclick="deleteDoc({{ d.id }}, this)">حذف</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
async function scanDuplicates(btn) {
btn.disabled = true;
const original = btn.textContent;
btn.textContent = 'جارٍ الفحص...';
const res = await fetch('/documents/scan-duplicates', { method: 'POST' });
if (res.ok) {
const data = await res.json();
alert(`اكتمل الفحص:\n- تم احتساب hash لـ ${data.hashed} وثيقة\n- تكرارات (نفس الصورة): ${data.flagged_by_hash}\n- تكرارات (نفس رقم الطلب): ${data.flagged_by_request}\n- الإجمالي المكرر: ${data.total_duplicates}`);
location.reload();
} else {
alert('فشل الفحص.');
btn.disabled = false;
btn.textContent = original;
}
}
async function deleteDoc(docId, btn) {
if (!confirm('حذف هذه الوثيقة المكررة؟')) return;
btn.disabled = true;
const res = await fetch(`/documents/${docId}`, { method: 'DELETE' });
if (res.ok) {
document.getElementById(`row-${docId}`).remove();
} else {
alert('فشل الحذف.');
btn.disabled = false;
}
}
async function deleteAllDuplicates(btn) {
if (!confirm('حذف جميع الوثائق المكررة نهائياً؟ (سيتم الاحتفاظ بالنسخة الأصلية)')) return;
btn.disabled = true;
btn.textContent = 'جارٍ الحذف...';
const res = await fetch('/documents/delete-duplicates', { method: 'POST' });
if (res.ok) {
const data = await res.json();
alert(`تم حذف ${data.deleted} وثيقة مكررة.`);
location.reload();
} else {
alert('فشل الحذف.');
btn.disabled = false;
}
}
</script>
{% endblock %}
+1 -1
View File
@@ -101,7 +101,7 @@
<div class="doc-thumbnails">
{% for d in documents %}
<a href="/review/{{ d.id }}" class="doc-thumb">
<img src="/uploads/{{ d.image_path }}" alt="وثيقة">
<img src="/thumbs/{{ d.image_path }}?size=480" alt="وثيقة" loading="lazy" decoding="async">
<div class="doc-thumb-info">
{% if d.request_number %}طلب {{ d.request_number }}{% endif %}
{% if d.request_date %}<br>{{ d.request_date }}{% endif %}