diff --git a/main.py b/main.py index 6df0810..eb8ebed 100644 --- a/main.py +++ b/main.py @@ -38,6 +38,24 @@ async def lifespan(app: FastAPI): cleanup_expired_sessions() except Exception: pass + # Resume any documents that were left in 'pending' state from a previous run + # (e.g. server restart killed the background extraction task). + try: + import asyncio + from database.connection import get_db + from services.extractor import get_default_provider + from routers.upload import _extract_and_save + + default_provider = get_default_provider() + with get_db() as conn: + stuck = conn.execute( + "SELECT id, image_path, provider FROM documents WHERE status='pending'" + ).fetchall() + for row in stuck: + provider = row["provider"] or default_provider + asyncio.create_task(_extract_and_save(row["id"], row["image_path"], provider)) + except Exception: + pass yield diff --git a/routers/documents.py b/routers/documents.py index 87cf243..8468110 100644 --- a/routers/documents.py +++ b/routers/documents.py @@ -233,6 +233,27 @@ async def retry_all_errors(): return JSONResponse({"ok": True, "retried": len(rows)}) +@router.post("/documents/resume-pending") +async def resume_pending(): + """Re-fire background extraction for any docs stuck in 'pending'. + Useful when a previous server restart killed in-flight extraction tasks.""" + import asyncio + from services.extractor import get_default_provider + from routers.upload import _extract_and_save + + default_provider = get_default_provider() + with get_db() as conn: + rows = conn.execute( + "SELECT id, image_path, provider FROM documents WHERE status='pending'" + ).fetchall() + + for row in rows: + provider = row["provider"] or default_provider + asyncio.create_task(_extract_and_save(row["id"], row["image_path"], provider)) + + return JSONResponse({"ok": True, "resumed": len(rows)}) + + @router.post("/documents/scan-duplicates") async def scan_duplicates(): """ diff --git a/routers/search.py b/routers/search.py index e0c3618..2e599a5 100644 --- a/routers/search.py +++ b/routers/search.py @@ -31,12 +31,9 @@ async def search( if property_number or district or block: properties = search_properties(property_number, district, block) - # If exactly one person found, preload their full details + # If exactly one person found, preload their full details (across all scopes) if len(persons) == 1 and not properties: - selected_person = get_person_with_properties( - persons[0]["id"], - persons[0].get("search_scope"), - ) + selected_person = get_person_with_properties(persons[0]["id"], None) return render_template( templates, diff --git a/services/search_service.py b/services/search_service.py index d489919..c2a6a6a 100644 --- a/services/search_service.py +++ b/services/search_service.py @@ -20,7 +20,7 @@ def _normalize_scope(text: str | None) -> str | None: def search_persons(query: str) -> list[dict]: - """Search persons by name, keeping separate result rows per search scope.""" + """Search persons by name. One row per person, aggregating all their search scopes.""" norm = normalize_arabic(query.strip()) pattern = f"%{norm}%" raw_pattern = f"%{query.strip()}%" @@ -30,9 +30,9 @@ def search_persons(query: str) -> list[dict]: rows = conn.execute( f""" SELECT p.*, - {scope_expr} AS search_scope, COUNT(DISTINCT pr.id) AS property_count, - COUNT(DISTINCT d.id) AS document_count + COUNT(DISTINCT d.id) AS document_count, + GROUP_CONCAT(DISTINCT {scope_expr}) AS search_scopes_raw FROM persons p LEFT JOIN documents d ON d.person_id = p.id LEFT JOIN properties pr ON pr.document_id = d.id @@ -41,14 +41,13 @@ def search_persons(query: str) -> list[dict]: OR p.father_name LIKE ? OR p.first_name LIKE ? OR p.family_name LIKE ? - GROUP BY p.id, {scope_expr} + GROUP BY p.id ORDER BY CASE WHEN p.first_name_norm = ? THEN 0 WHEN p.family_name_norm = ? THEN 0 ELSE 1 END, p.first_name, - p.family_name, - search_scope + p.family_name """, (pattern, pattern, raw_pattern, raw_pattern, raw_pattern, norm, norm), ).fetchall() @@ -56,7 +55,17 @@ def search_persons(query: str) -> list[dict]: results = [] for row in rows: person = dict(row) - person["search_scope"] = _normalize_scope(person.get("search_scope")) + raw = person.pop("search_scopes_raw", None) or "" + scopes = [s.strip() for s in raw.split(",") if s and s.strip()] + # Deduplicate while preserving order + seen = set() + unique_scopes = [] + for s in scopes: + if s not in seen: + seen.add(s) + unique_scopes.append(s) + person["search_scopes"] = unique_scopes + person["search_scope"] = "، ".join(unique_scopes) if unique_scopes else None results.append(person) return results diff --git a/templates/documents.html b/templates/documents.html index 8952f4a..09be30a 100644 --- a/templates/documents.html +++ b/templates/documents.html @@ -58,6 +58,12 @@ {% endif %} +{% if stats.processing %} +
+{% endif %} + {% if stats.pending_review %}