From f6c87e761eeda3e6146293edcb17b10ebbcc7602 Mon Sep 17 00:00:00 2001 From: Georges Haddad Date: Wed, 22 Apr 2026 12:58:35 +0300 Subject: [PATCH] Mobile UI: compact card layout for tables; switch Gemini default to 2.5-pro --- config.py | 6 +- debug_query.py | 40 +++++++++ routers/review.py | 25 ++++++ static/css/main.css | 211 +++++++++++++++++++++++++++++--------------- 4 files changed, 209 insertions(+), 73 deletions(-) create mode 100644 debug_query.py diff --git a/config.py b/config.py index 3f8ed90..b34e75a 100644 --- a/config.py +++ b/config.py @@ -20,8 +20,10 @@ SECRET_KEY = os.environ.get("SECRET_KEY", "") DATABASE_PATH = os.environ.get("DB_PATH", str(BASE_DIR / "data" / "realestate.db")) UPLOAD_DIR = os.environ.get("UPLOAD_DIR", str(BASE_DIR / "uploads")) MAX_CONCURRENT_EXTRACTIONS = int(os.environ.get("MAX_CONCURRENT", "3")) -CLAUDE_MODEL = "claude-sonnet-4-6" -GEMINI_MODEL = "gemini-2.5-flash" +CLAUDE_MODEL = os.environ.get("CLAUDE_MODEL", "claude-sonnet-4-6") +# Gemini 2.5 Pro is more reliable than Flash for Arabic OCR/reasoning +# (Flash occasionally returns refusals / "absolute" style hedges). Override via env if needed. +GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-pro") # Production mode — set to "production" on VPS ENVIRONMENT = os.environ.get("ENVIRONMENT", "development") diff --git a/debug_query.py b/debug_query.py new file mode 100644 index 0000000..5ebc28a --- /dev/null +++ b/debug_query.py @@ -0,0 +1,40 @@ +import sqlite3 +conn = sqlite3.connect('data/realestate.db') +conn.row_factory = sqlite3.Row + +# All documents +rows = conn.execute(''' + SELECT d.id, d.person_id, d.request_number, d.search_scope, d.page_info, + d.page_number, d.pdf_group_id, d.status, + p.first_name, p.family_name + FROM documents d + LEFT JOIN persons p ON p.id = d.person_id + ORDER BY d.id +''').fetchall() + +print("=== ALL documents ===") +for r in rows: + d = dict(r) + print("id=%s status=%s person_id=%s req=%r scope=%r page_info=%r pdf=%r name=%s %s" % ( + d['id'], d['status'], d['person_id'], d['request_number'], + d['search_scope'], d['page_info'], d['pdf_group_id'], + d['first_name'], d['family_name'])) + +# Persons +print("\n=== Persons ===") +persons = conn.execute('SELECT id, first_name, father_name, family_name FROM persons ORDER BY id').fetchall() +for p in persons: + d = dict(p) + print("id=%s %s %s %s" % (d['id'], d['first_name'], d['father_name'], d['family_name'])) + +# Potential duplicate persons +print("\n=== Potential duplicate persons ===") +dups = conn.execute(''' + SELECT p1.id AS pid1, p2.id AS pid2, p1.first_name, p1.family_name + FROM persons p1 + JOIN persons p2 ON p1.id < p2.id + AND p1.first_name = p2.first_name + AND COALESCE(p1.family_name,'') = COALESCE(p2.family_name,'') +''').fetchall() +for d in dups: + print(dict(d)) diff --git a/routers/review.py b/routers/review.py index 002303a..9aba7b0 100644 --- a/routers/review.py +++ b/routers/review.py @@ -70,6 +70,16 @@ def _find_page1_candidate(conn, doc: dict) -> tuple[bool, dict | None]: LIMIT 1""", (req_num, doc_scope, doc["id"]), ).fetchone() + else: + sibling_doc = conn.execute( + """SELECT * FROM documents + WHERE request_number=? AND (search_scope IS NULL OR TRIM(search_scope)='') AND id != ? + ORDER BY CASE WHEN page_number=1 THEN 0 ELSE 1 END, + CASE WHEN person_id IS NOT NULL THEN 0 ELSE 1 END, + id + LIMIT 1""", + (req_num, doc["id"]), + ).fetchone() return is_subsequent_page, dict(sibling_doc) if sibling_doc else None @@ -727,6 +737,11 @@ async def retrigger_extraction(doc_id: int, provider: str = ""): use_provider = provider or doc["provider"] or get_default_provider() with get_db() as conn: + old_person_id = conn.execute( + "SELECT person_id FROM documents WHERE id=?", (doc_id,) + ).fetchone() + old_person_id = old_person_id["person_id"] if old_person_id else None + conn.execute( """UPDATE documents SET status='pending', @@ -750,6 +765,16 @@ async def retrigger_extraction(doc_id: int, provider: str = ""): ) conn.execute("DELETE FROM properties WHERE document_id=?", (doc_id,)) + # Clean up orphaned person if this was their only document + if old_person_id: + remaining = conn.execute( + "SELECT COUNT(*) AS n FROM documents WHERE person_id=?", + (old_person_id,), + ).fetchone()["n"] + if remaining == 0: + conn.execute("DELETE FROM properties WHERE person_id=?", (old_person_id,)) + conn.execute("DELETE FROM persons WHERE id=?", (old_person_id,)) + from routers.upload import _extract_and_save asyncio.create_task(_extract_and_save(doc_id, doc["image_path"], use_provider)) return JSONResponse({"ok": True, "message": "Extraction started"}) diff --git a/static/css/main.css b/static/css/main.css index b878c8b..6a515d6 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -1382,77 +1382,6 @@ a.stat.active { border-color: rgba(15,118,110,.24); background: var(--primary-so .doc-thumb img { height: 70px; } .stats-bar .stat { flex-basis: 100%; } - .responsive-table { - border-collapse: separate; - border-spacing: 0; - background: transparent; - } - .responsive-table thead { - display: none; - } - .responsive-table tbody, - .responsive-table tr, - .responsive-table td { - display: block; - width: 100%; - } - .responsive-table tbody { - padding: .35rem .75rem .85rem; - } - .responsive-table tr { - margin-bottom: .75rem; - border: 1px solid rgba(23,37,44,.08); - border-radius: 18px; - background: rgba(255,255,255,.96); - box-shadow: 0 10px 24px rgba(23,37,44,.08); - overflow: hidden; - } - .responsive-table td { - display: grid; - grid-template-columns: minmax(92px, 110px) 1fr; - gap: .65rem; - align-items: center; - padding: .7rem .9rem; - text-align: right; - border-bottom: 1px solid rgba(23,37,44,.06); - } - .responsive-table td:last-child { - border-bottom: none; - } - .responsive-table td[data-label]::before { - display: block; - color: var(--text-muted); - font-size: .76rem; - font-weight: 700; - } - .responsive-table .doc-actions, - .responsive-table td[data-label="الإجراءات"] { - display: flex; - flex-wrap: wrap; - justify-content: stretch; - gap: .5rem; - } - .responsive-table td[data-label="الإجراءات"]::before { - width: 100%; - } - .responsive-table .doc-actions .btn, - .responsive-table td[data-label="الإجراءات"] .btn, - .responsive-table td[data-label="الإجراءات"] .inline-form, - .responsive-table td[data-label="الإجراءات"] .inline-form .btn { - flex: 1 1 100%; - width: 100%; - } - .responsive-table .doc-thumb-mini { - width: 72px; - height: 54px; - } - .ownership-alert td { - background: transparent; - } - .ownership-alert { - border-color: #fecaca; - background: #fff8f8; - } .mobile-dock { right: .5rem; left: .5rem; @@ -1465,6 +1394,146 @@ a.stat.active { border-color: rgba(15,118,110,.24); background: var(--primary-so } } +/* ============================ + Responsive tables — compact list cards on mobile/tablet + Each row becomes a card with property number prominent on top + and other fields laid out 2-per-row for density. + ============================ */ +@media (max-width: 640px) { + .responsive-table { + border-collapse: separate; + border-spacing: 0; + background: transparent; + } + .responsive-table thead { display: none; } + .responsive-table tbody, + .responsive-table tr, + .responsive-table td { display: block; width: 100%; } + + .responsive-table tbody { padding: .25rem .5rem .6rem; } + + .responsive-table tr { + display: grid; + grid-template-columns: 1fr 1fr; + row-gap: 0; + column-gap: .5rem; + margin-bottom: .55rem; + padding: .5rem .65rem .6rem; + border: 1px solid rgba(23,37,44,.08); + border-radius: 14px; + background: rgba(255,255,255,.96); + box-shadow: 0 6px 16px rgba(23,37,44,.06); + overflow: hidden; + } + + .responsive-table td { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: .1rem; + padding: .3rem .15rem; + text-align: right; + border: none; + font-size: .85rem; + line-height: 1.3; + min-width: 0; + word-break: break-word; + } + .responsive-table td[data-label]::before { + display: block; + content: attr(data-label); + color: var(--text-muted); + font-size: .68rem; + font-weight: 700; + letter-spacing: .01em; + margin-bottom: .05rem; + } + + /* Prominent / full-width fields */ + .responsive-table td[data-label="رقم العقار"] { + grid-column: 1 / -1; + order: -1; + padding-top: .15rem; + padding-bottom: .4rem; + border-bottom: 1px solid rgba(23,37,44,.08); + margin-bottom: .3rem; + } + .responsive-table td[data-label="رقم العقار"]::before { + font-size: .7rem; + } + .responsive-table td[data-label="رقم العقار"] { + font-size: 1.15rem; + font-weight: 800; + color: var(--primary-dark); + } + + .responsive-table td[data-label="المالك"], + .responsive-table td[data-label="اسم الفريق"], + .responsive-table td[data-label="المنطقة العقارية"], + .responsive-table td[data-label="الإجراءات"] { + grid-column: 1 / -1; + } + + /* Row index: small corner badge, hide label */ + .responsive-table td[data-label="#"] { + order: -2; + grid-column: 1 / -1; + flex-direction: row; + align-items: center; + padding: 0 0 .15rem; + font-size: .7rem; + color: var(--text-muted); + font-weight: 700; + } + .responsive-table td[data-label="#"]::before { + content: '#'; + margin-inline-end: .25rem; + color: var(--text-muted); + font-size: .7rem; + } + + /* Ownership-alert rows stay visually flagged */ + .ownership-alert { + border-color: #fecaca !important; + background: #fff8f8 !important; + } + .ownership-alert td { background: transparent; } + + /* Actions cell: buttons flow full width */ + .responsive-table .doc-actions, + .responsive-table td[data-label="الإجراءات"] { + flex-direction: row; + flex-wrap: wrap; + align-items: stretch; + gap: .4rem; + padding-top: .4rem; + border-top: 1px dashed rgba(23,37,44,.08); + margin-top: .2rem; + } + .responsive-table td[data-label="الإجراءات"]::before { + width: 100%; + margin-bottom: .2rem; + } + .responsive-table .doc-actions .btn, + .responsive-table td[data-label="الإجراءات"] .btn, + .responsive-table td[data-label="الإجراءات"] .inline-form, + .responsive-table td[data-label="الإجراءات"] .inline-form .btn { + flex: 1 1 calc(50% - .2rem); + min-width: 0; + font-size: .8rem; + } + .responsive-table .doc-thumb-mini { + width: 64px; + height: 48px; + } + + /* Table wrapper padding tweaks */ + .table-wrapper.table-wrapper-spaced, + .table-wrapper { + padding: 0; + } +} + /* ============================ Upload & Staging (mobile-first) ============================ */