perf: paginate docs list, lazy thumbnails, static cache headers
This commit is contained in:
@@ -18,6 +18,7 @@ def _migrate(conn):
|
|||||||
("declared_property_count", "INTEGER"),
|
("declared_property_count", "INTEGER"),
|
||||||
("image_hash", "TEXT"),
|
("image_hash", "TEXT"),
|
||||||
("duplicate_of", "INTEGER"),
|
("duplicate_of", "INTEGER"),
|
||||||
|
("duplicate_dismissed", "INTEGER DEFAULT 0"),
|
||||||
]
|
]
|
||||||
for col, col_type in migrations:
|
for col, col_type in migrations:
|
||||||
if col not in existing:
|
if col not in existing:
|
||||||
|
|||||||
+12
-5
@@ -281,7 +281,8 @@ async def scan_duplicates():
|
|||||||
for g in hash_groups:
|
for g in hash_groups:
|
||||||
result = conn.execute(
|
result = conn.execute(
|
||||||
"""UPDATE documents SET duplicate_of=?
|
"""UPDATE documents SET duplicate_of=?
|
||||||
WHERE image_hash=? AND id != ? AND status != 'staged'""",
|
WHERE image_hash=? AND id != ? AND status != 'staged'
|
||||||
|
AND COALESCE(duplicate_dismissed, 0) = 0""",
|
||||||
(g["keeper"], g["image_hash"], g["keeper"]),
|
(g["keeper"], g["image_hash"], g["keeper"]),
|
||||||
)
|
)
|
||||||
flagged_by_hash += result.rowcount or 0
|
flagged_by_hash += result.rowcount or 0
|
||||||
@@ -307,6 +308,7 @@ async def scan_duplicates():
|
|||||||
AND COALESCE(TRIM(page_info),'')=?
|
AND COALESCE(TRIM(page_info),'')=?
|
||||||
AND id != ?
|
AND id != ?
|
||||||
AND duplicate_of IS NULL
|
AND duplicate_of IS NULL
|
||||||
|
AND COALESCE(duplicate_dismissed, 0) = 0
|
||||||
AND status IN ('extracted','confirmed')""",
|
AND status IN ('extracted','confirmed')""",
|
||||||
(g["keeper"], g["rn"], g["sc"], g["pi"], g["keeper"]),
|
(g["keeper"], g["rn"], g["sc"], g["pi"], g["keeper"]),
|
||||||
)
|
)
|
||||||
@@ -395,16 +397,21 @@ async def delete_all_duplicates():
|
|||||||
|
|
||||||
@router.post("/documents/{doc_id}/unflag-duplicate")
|
@router.post("/documents/{doc_id}/unflag-duplicate")
|
||||||
async def unflag_duplicate(doc_id: int):
|
async def unflag_duplicate(doc_id: int):
|
||||||
"""Mark a flagged-duplicate document as NOT a duplicate (clear duplicate_of)."""
|
"""Mark a flagged-duplicate document as NOT a duplicate (clear duplicate_of
|
||||||
|
and remember the decision so future scans don't re-flag it)."""
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"SELECT id FROM documents WHERE id=? AND duplicate_of IS NOT NULL",
|
"SELECT id FROM documents WHERE id=?",
|
||||||
(doc_id,),
|
(doc_id,),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
return JSONResponse({"error": "not flagged"}, status_code=404)
|
return JSONResponse({"error": "not found"}, status_code=404)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE documents SET duplicate_of=NULL, updated_at=CURRENT_TIMESTAMP WHERE id=?",
|
"""UPDATE documents
|
||||||
|
SET duplicate_of=NULL,
|
||||||
|
duplicate_dismissed=1,
|
||||||
|
updated_at=CURRENT_TIMESTAMP
|
||||||
|
WHERE id=?""",
|
||||||
(doc_id,),
|
(doc_id,),
|
||||||
)
|
)
|
||||||
return JSONResponse({"ok": True})
|
return JSONResponse({"ok": True})
|
||||||
|
|||||||
+10
-1
@@ -35,11 +35,13 @@ def _hash_file(path: Path) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _find_duplicate(conn, image_hash: str) -> dict | None:
|
def _find_duplicate(conn, image_hash: str) -> dict | None:
|
||||||
"""Return an existing non-staged document sharing the same image hash."""
|
"""Return an existing non-staged document sharing the same image hash.
|
||||||
|
Skips rows the user has explicitly dismissed as 'not a duplicate'."""
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"""SELECT id, status, image_path, person_id, request_number
|
"""SELECT id, status, image_path, person_id, request_number
|
||||||
FROM documents
|
FROM documents
|
||||||
WHERE image_hash=? AND status != 'staged'
|
WHERE image_hash=? AND status != 'staged'
|
||||||
|
AND COALESCE(duplicate_dismissed, 0) = 0
|
||||||
ORDER BY id LIMIT 1""",
|
ORDER BY id LIMIT 1""",
|
||||||
(image_hash,),
|
(image_hash,),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
@@ -126,10 +128,17 @@ async def _extract_and_save(doc_id: int, image_path: str, provider: str = ""):
|
|||||||
AND COALESCE(search_scope,'')=?
|
AND COALESCE(search_scope,'')=?
|
||||||
AND COALESCE(page_info,'')=?
|
AND COALESCE(page_info,'')=?
|
||||||
AND status IN ('extracted','confirmed')
|
AND status IN ('extracted','confirmed')
|
||||||
|
AND COALESCE(duplicate_dismissed, 0) = 0
|
||||||
ORDER BY id LIMIT 1""",
|
ORDER BY id LIMIT 1""",
|
||||||
(doc_id, req_num, scope, page_info),
|
(doc_id, req_num, scope, page_info),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if existing:
|
if existing:
|
||||||
|
# Only auto-flag if THIS document hasn't itself been dismissed.
|
||||||
|
self_row = conn.execute(
|
||||||
|
"SELECT COALESCE(duplicate_dismissed, 0) AS d FROM documents WHERE id=?",
|
||||||
|
(doc_id,),
|
||||||
|
).fetchone()
|
||||||
|
if not (self_row and self_row["d"]):
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE documents SET duplicate_of=? WHERE id=?",
|
"UPDATE documents SET duplicate_of=? WHERE id=?",
|
||||||
(existing["id"], doc_id),
|
(existing["id"], doc_id),
|
||||||
|
|||||||
@@ -206,15 +206,26 @@ async function deleteDoc(docId, btn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function unflagDuplicate(docId, btn) {
|
async function unflagDuplicate(docId, btn) {
|
||||||
if (!confirm('تأكيد أنها ليست وثيقة مكررة؟')) return;
|
const original = btn.textContent;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
const res = await fetch(`/documents/${docId}/unflag-duplicate`, { method: 'POST' });
|
btn.textContent = '...';
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/documents/${docId}/unflag-duplicate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Accept': 'application/json' },
|
||||||
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
document.getElementById(`card-${docId}`)?.remove();
|
document.getElementById(`card-${docId}`)?.remove();
|
||||||
} else {
|
return;
|
||||||
alert('فشل التحديث.');
|
|
||||||
btn.disabled = false;
|
|
||||||
}
|
}
|
||||||
|
let msg = `HTTP ${res.status}`;
|
||||||
|
try { const j = await res.json(); if (j.error) msg += ` — ${j.error}`; } catch (_) {}
|
||||||
|
alert(`فشل التحديث: ${msg}`);
|
||||||
|
} catch (e) {
|
||||||
|
alert(`خطأ في الشبكة: ${e.message}`);
|
||||||
|
}
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = original;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAllDuplicates(btn) {
|
async function deleteAllDuplicates(btn) {
|
||||||
|
|||||||
Reference in New Issue
Block a user