CoolFace
Apppublic

JonoRens/gr_test_app

sourceHugging Faceupdated 11mo agoView on Hugging Face
0likes
accesspoints_module_hf.py972 linesDownload Raw Back to root
1import gradio as gr2import psycopg23import pandas as pd4import random5import re6import io7from pathlib import Path8from PIL import Image, ImageDraw, ImageFont9import PIL10from math import sqrt11import pickle12from cryptography.fernet import Fernet, InvalidToken13 14from typing import Optional, Dict, Any15import time16import json17import os18 19 20# ===== Optional QR: try qrcode, else Pillow fallback (no crash on Spaces) =====21try:22    import qrcode  # external; may not exist on Spaces23    def _make_qr_image(data: str, box_size: int = 20, border: int = 2) -> Image.Image:24        qr = qrcode.QRCode(version=1, box_size=box_size, border=border)25        qr.add_data(data)26        qr.make(fit=True)27        return qr.make_image(fill_color="#000000", back_color="#FFFFFF").convert("RGB")28except Exception:29    # Fallback: draw a simple placeholder (NOT a real QR) so app keeps working30    def _make_qr_image(data: str, box_size: int = 20, border: int = 2) -> Image.Image:31        w = h = 40032        img = Image.new("RGB", (w, h), "white")33        d = ImageDraw.Draw(img)34        d.rectangle([0, 0, w - 1, h - 1], outline="black")35        msg = "QR lib missing"36        d.text((12, 12), msg, fill="black")37        d.text((12, 40), str(data), fill="black")38        return img39 40 41# ===== Hugging Face / app.py glue =====42_CACHED_NHOST_PARAMS: Dict[str, Any] = {}43 44def _params_from_env() -> Optional[Dict[str, Any]]:45    j = os.environ.get("NHOST_JSON", "").strip()46    if j:47        try:48            obj = json.loads(j)49            return {50                "host": obj.get("host"),51                "port": int(obj.get("port", 5432)) if obj.get("port") else None,52                "user": obj.get("user"),53                "password": obj.get("password"),54                "dbname": obj.get("dbname"),55                "sslmode": obj.get("sslmode", "require"),56            }57        except Exception:58            pass59 60    host = os.environ.get("NHOST_HOST", "").strip()61    user = os.environ.get("NHOST_USER", "").strip()62    password = os.environ.get("NHOST_PASSWORD", "").strip()63    db = os.environ.get("NHOST_DB", "").strip() or os.environ.get("NHOST_DBNAME", "").strip()64    port = os.environ.get("NHOST_PORT", "").strip()65    sslmode = os.environ.get("NHOST_SSLMODE", "require").strip() or "require"66 67    if host and user and password and db:68        return {69            "host": host,70            "port": int(port) if port else 5432,71            "user": user,72            "password": password,73            "dbname": db,74            "sslmode": sslmode,75        }76    return None77 78 79def decrypt_nhost(enc_path: Path = None) -> Dict[str, Any]:80    global _CACHED_NHOST_PARAMS81    if _CACHED_NHOST_PARAMS:82        return _CACHED_NHOST_PARAMS83 84    env_params = _params_from_env()85    if env_params and env_params.get("host"):86        _CACHED_NHOST_PARAMS = env_params87        return _CACHED_NHOST_PARAMS88 89    if enc_path is None:90        here = Path(__file__).parent91        enc_path = (Path.cwd() / "nhost_params.enc")92        if not enc_path.exists():93            enc_path = here / "nhost_params.enc"94 95    secret = os.environ.get("SECRET_KEY", "").strip()96    try:97        if not secret:98            raise RuntimeError("SECRET_KEY env var is missing.")99        if not enc_path.exists():100            raise RuntimeError("nhost_params.enc not found.")101        key = secret.encode("utf-8")102        f = Fernet(key)103        blob = enc_path.read_bytes()104        raw = f.decrypt(blob)105        obj = json.loads(raw.decode("utf-8"))106 107        _CACHED_NHOST_PARAMS = {108            "host": obj.get("host"),109            "port": int(obj.get("port", 5432)) if obj.get("port") else 5432,110            "user": obj.get("user"),111            "password": obj.get("password"),112            "dbname": obj.get("dbname"),113            "sslmode": obj.get("sslmode", "require"),114        }115        return _CACHED_NHOST_PARAMS116    except Exception as e:117        print("[WARN] decrypt_nhost(): could not decrypt nhost_params.enc →", e)118 119    raise RuntimeError(120        "No NHOST connection params found. "121        "Set NHOST_JSON (or NHOST_* env vars) or provide nhost_params.enc with SECRET_KEY."122    )123 124 125def get_connection() -> psycopg2.extensions.connection:126    params = decrypt_nhost()127    if not params.get("host"):128        raise RuntimeError("NHOST params invalid (no host).")129    return psycopg2.connect(**params)130 131 132# ─────────────────────────────────────────────133# Active User: same approach as property/permissions modules134# ─────────────────────────────────────────────135 136NEW_DATA_FILE = Path(__file__).parent / "openqr_active_user.pkl.enc"137NEW_KEY_FILE  = Path(__file__).parent / "openqr_secret.key"138LEGACY_DATA_FILE = Path(__file__).parent / "oqrdata.pkl.enc"139LEGACY_KEY_FILE  = Path(__file__).parent / "oqrdata.key"140 141def _read_encrypted_pickle(data_path: Path, key_path: Path):142    try:143        if not data_path.exists() or not key_path.exists():144            return None145        key = key_path.read_bytes()146        f = Fernet(key)147        blob = data_path.read_bytes()148        raw = f.decrypt(blob)149#        import pickle150        return pickle.loads(raw)151    except (InvalidToken, Exception):152        return None153 154 155ActiveUserID: Optional[int] = None156 157 158def _coerce_uid(val) -> Optional[int]:159    try:160        iv = int(str(val).strip())161        return iv if iv > 0 else None162    except Exception:163        return None164    165def resolve_and_cache_uid(get_user_id=None) -> Optional[int]:166    global ActiveUserID167    if _coerce_uid(ActiveUserID):168        return ActiveUserID169 170    # 1) callback first171    if callable(get_user_id):172        try:173            v = _coerce_uid(get_user_id())174            if v:175                ActiveUserID = v176                return ActiveUserID177        except Exception:178            pass179 180    # 2) env next181    env_val = _coerce_uid(os.getenv("OPENQR_ACTIVE_USER_ID") or os.getenv("OQR_ACTIVE_USER_ID"))182    if env_val:183        ActiveUserID = env_val184        return ActiveUserID185 186    # 3) encrypted pickle last187    info = _read_encrypted_pickle(NEW_DATA_FILE, NEW_KEY_FILE) or _read_encrypted_pickle(LEGACY_DATA_FILE, LEGACY_KEY_FILE)188    if info:189        for k in ("ActiveUserID", "UserID", "MUserID"):190            v = _coerce_uid(info.get(k))191            if v:192                ActiveUserID = v193                return ActiveUserID194 195    ActiveUserID = None196    return None197 198 199def launch_accesspoints_module(get_user_id=None):200 201    # ========== UTILS ==========202    def generate_id():203        return random.randint(111111111111111, 999999999999999)204 205    def _get_installers_flag(user_id: int | None) -> bool:206        if not user_id:207            return False208        try:209            conn = get_connection()210            cur = conn.cursor()211            cur.execute('SELECT "Installers" FROM managerusers WHERE "MUserID" = %s', (int(user_id),))212            row = cur.fetchone()213            cur.close()214            conn.close()215            return bool(row[0]) if row and row[0] is not None else False216        except Exception as e:217            print(f"⚠️ Installers check failed: {e}")218            return False219 220#    IsInstaller = _get_installers_flag(_uid())221 222    def _get_property_name(prop_id: int | str, user_id: int | None) -> str:223        try:224            conn = get_connection()225            cur = conn.cursor()226            cur.execute(227                'SELECT "Name" FROM property WHERE "PropID" = %s AND "UserID" = %s AND "Active" = TRUE',228                (int(prop_id), int(user_id) if user_id is not None else None),229            )230            row = cur.fetchone()231            cur.close()232            conn.close()233            return row[0] if row and row[0] is not None else ""234        except Exception as e:235            print(f"⚠️ _get_property_name failed: {e}")236            return ""237 238    def _get_font(size: int, font_path: str | None = None) -> ImageFont.FreeTypeFont:239        candidates: list[str | Path] = []240        if font_path:241            candidates.append(font_path)242        pil_fonts = Path(PIL.__file__).parent / "fonts"243        candidates += [pil_fonts / "DejaVuSans.ttf", pil_fonts / "DejaVuSans-Bold.ttf"]244        candidates += [245            "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",246            "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",247            "/Library/Fonts/Arial.ttf",248            "/System/Library/Fonts/Supplemental/Arial.ttf",249            r"C:\Windows\Fonts\arial.ttf",250        ]251        for p in candidates:252            try:253                p = Path(p)254                if p.exists():255                    return ImageFont.truetype(str(p), size)256            except Exception:257                pass258        return ImageFont.load_default()259 260    # ---------- Geometry helper (flat-top regular hex that fits a bbox) ----------261    def hex_points_flat_top(width: int, height: int):262        A = height / 2.0  # apothem263        W_expected = (4.0 * A) / sqrt(3)264        scale_x = width / W_expected265        top_half = (A / sqrt(3)) * scale_x266        cx, cy = width / 2.0, height / 2.0267        pts = [268            (cx - top_half, 0),          # top-left269            (cx + top_half, 0),          # top-right270            (width, cy),                 # right-mid271            (cx + top_half, height),     # bottom-right272            (cx - top_half, height),     # bottom-left273            (0, cy),                     # left-mid274        ]275        return [(int(round(x)), int(round(y))) for (x, y) in pts]276 277    def create_save_png_qrcode(278        data: str,279        *,280        border_thickness: int = 60,281        border_color: str = "#EF5635",282        top_text: str = "RyGo QR",283        bottom_text: str = "Scan to open",284        top_font_size: int = 50,285        bottom_font_size: int = 50,286        text_color: str = "#000000",287        font_path: str | None = None,288        box_size: int = 20,289        qr_border_modules: int = 2290    ) -> bytes:291        if text_color is None:292            text_color = border_color293 294        # Use optional qrcode lib if available, else fallback placeholder295        qr_img = _make_qr_image(str(data), box_size=box_size, border=qr_border_modules)296        qr_w, qr_h = qr_img.size297 298        font_top = _get_font(top_font_size, font_path)299        font_bottom = _get_font(bottom_font_size, font_path)300        tmp = Image.new("RGB", (10, 10))301        dtmp = ImageDraw.Draw(tmp)302 303        def tsize(s, f):304            if not s:305                return (0, 0)306            l, t, r, b = dtmp.textbbox((0, 0), s, font=f)307            return (r - l, b - t)308 309        top_w, top_h = tsize(top_text, font_top)310        bot_w, bot_h = tsize(bottom_text, font_bottom)311 312        pad_top = max(8, int(top_font_size * 0.35))313        pad_bot = max(8, int(bottom_font_size * 0.35))314        top_band_h = (top_h + 2 * pad_top) if top_text else 0315        bottom_band_h = (bot_h + 2 * pad_bot) if bottom_text else 0316 317        inner_content_w = max(qr_w, top_w, bot_w)318        inner_content_h = top_band_h + qr_h + bottom_band_h319 320        need_top = (max(top_w, bot_w)) * (sqrt(3) / 2.0)321        need_center = inner_content_w * (sqrt(3) / 4.0)322        need_height = inner_content_h / 2.0323        A_in = int(round(max(need_top, need_center, need_height)))324 325        inner_w = int(round((4.0 * A_in) / sqrt(3)))326        inner_h = int(round(2.0 * A_in))327 328        A_out = A_in + border_thickness329        outer_w = int(round((4.0 * A_out) / sqrt(3)))330        outer_h = int(round(2.0 * A_out))331 332        canvas = Image.new("RGB", (outer_w, outer_h), "white")333        draw = ImageDraw.Draw(canvas)334        outer_hex = hex_points_flat_top(outer_w, outer_h)335        draw.polygon(outer_hex, fill=border_color)336 337        inner_offset = ((outer_w - inner_w) // 2, (outer_h - inner_h) // 2)338        inner_hex_abs = [(x + inner_offset[0], y + inner_offset[1]) for x, y in hex_points_flat_top(inner_w, inner_h)]339        draw.polygon(inner_hex_abs, fill="white")340 341        inner_img = Image.new("RGB", (inner_w, inner_h), "white")342        idraw = ImageDraw.Draw(inner_img)343 344        if top_text:345            tx = (inner_w - top_w) // 2346            ty = (top_band_h - top_h) // 2347            idraw.text((tx, ty), top_text, font=font_top, fill=text_color)348 349        qr_x = (inner_w - qr_w) // 2350        qr_y = top_band_h351        inner_img.paste(qr_img, (qr_x, qr_y))352 353        if bottom_text:354            bx = (inner_w - bot_w) // 2355            by = top_band_h + qr_h + (bottom_band_h - bot_h) // 2356            idraw.text((bx, by), bottom_text, font=font_bottom, fill=text_color)357 358        mask_local = Image.new("L", (inner_w, inner_h), 0)359        mdraw = ImageDraw.Draw(mask_local)360        mdraw.polygon(hex_points_flat_top(inner_w, inner_h), fill=255)361        canvas.paste(inner_img, inner_offset, mask_local)362 363        buf = io.BytesIO()364        canvas.save(buf, format="PNG")365        return buf.getvalue()366 367    # ========== MAIN MODULE ==========368    with gr.Blocks() as demo:369 370        with gr.Tab("Manage Access Points"):371 372            # ---- property options for current user ----373            def load_installer_options():374                conn = get_connection()375                cur = conn.cursor()376                cur.execute(377                    'SELECT "UserCompanyID","UserCompany" '378                    'FROM managerusers '379                    'WHERE "Installers" = TRUE '380                    'ORDER BY "UserCompany" ASC'381                )382                rows = cur.fetchall()383                cur.close()384                conn.close()385                opts: list[str] = ["<to be appointed>"]386                for cid, cname in rows:387                    if cid is None or cname is None:388                        continue389                    opts.append(f"{int(cid)} | {str(cname)}")390                return opts391 392            def load_property_options():393                uid = _uid()394                print(f"DEBUG load_property(): ActiveUserID={uid}")395                396                if uid is None:397                    return []398                conn = get_connection()399                cursor = conn.cursor()400                cursor.execute(401                    'SELECT "PropID", "Name", "Owner" '402                    'FROM property WHERE "UserID" = %s AND "Active" = TRUE',403                    (uid,)404                )405                rows = cursor.fetchall()406                conn.close()407                seen = set()408                options = []409                for prop_id, name, owner in rows:410                    key = (prop_id, name, owner)411                    if key not in seen:412                        seen.add(key)413                        label = f"{name} ({owner}) [{prop_id}]"414                        value = f"{prop_id}|{name}|{owner}"415                        options.append((label, value))416                return options417            def _uid() -> Optional[int]:418                # convenience wrapper used throughout the module419                return resolve_and_cache_uid(get_user_id)420 421            def _prime_uid_then_fill_properties():422                # allow time for env/callback to be ready (HF cold start)423                deadline = time.time() + 10.0424                while resolve_and_cache_uid(get_user_id) is None and time.time() < deadline:425                    time.sleep(0.1)426 427                uid = _uid()428                print("DEBUG (prime): starting with UID =", uid)429 430                if uid is None:431                    return gr.update(choices=[], value=None, interactive=False)432 433                # Use options list [(label, value), ...]; build prop_map (label -> (PropID, Name, Owner))434                options = load_property_options()435                labels = [lbl for (lbl, _val) in options]436 437                prop_map.clear()438                for lbl, val in options:439                    try:440                        pid, pname, owner = val.split("|", 2)441                    except ValueError:442                        pid, pname, owner = "", "", ""443                    prop_map[lbl] = (int(pid) if pid else None, pname, owner)444 445                return gr.update(choices=labels, value=None, interactive=bool(labels))446 447            def sync_accesspoints(prop_id):448                conn = get_connection()449                cursor = conn.cursor()450                cursor.execute(451                    'SELECT "ID","PropID","NameOfAccessPoint","AccessPointID","UserID",'452                    '       "RestrictedAP","APDeviceName","APInstaller","APInstallerID","ApInOut" '453                    'FROM accesspoint '454                    'WHERE "UserID" = %s AND "PropID" = %s '455                    'ORDER BY lower("NameOfAccessPoint") ASC',456                    (_uid(), int(prop_id))457                )458                rows = cursor.fetchall()459                conn.close()460 461                df = pd.DataFrame(462                    rows,463                    columns=[464                        "ID","PropID","NameOfAccessPoint","AccessPointID","UserID",465                        "RestrictedAP","APDeviceName","APInstaller","APInstallerID","ApInOut"466                    ],467                )468                if not df.empty:469                    df["AccessPointID"] = df["AccessPointID"].astype(str).str.replace(r"\.0$", "", regex=True)470 471                df_display = df.drop(columns=["ID", "PropID", "UserID"])472                if not df_display.empty:473                    desired_front = [c for c in ["NameOfAccessPoint", "ApInOut", "AccessPointID"] if c in df_display.columns]474                    others = [c for c in df_display.columns if c not in desired_front]475                    df_display = df_display[desired_front + others]476                return df, df_display477 478            def get_row_options(df):479                if df is None or df.empty:480                    return []481                return [482                    (f"{row['NameOfAccessPoint']} - {row.get('ApInOut','')}", str(int(row["ID"])))483                    for _, row in df.iterrows()484                ]485 486            def select_property(label):487                if not label or label not in prop_map:488                    return (489                        gr.update(visible=False),  # accesspoint_table490                        None, None,491                        gr.update(choices=[], value=None, interactive=False, visible=True),492                        gr.update(choices=[], value=None, interactive=False, visible=True),493                        gr.update(visible=False),  # apply_edit_btn494                        gr.update(visible=False, interactive=False),  # delete_btn495                        gr.update(visible=False, interactive=False),  # add_btn496                        gr.update(visible=False),  # edit_group497                        gr.update(value="", visible=False),  # error_msg498                        gr.update(visible=False),  # cancel_edit_btn499                        gr.update(visible=False),  # cancel_delete_btn500                    )501 502                prop_id, prop_name, owner = prop_map[label]503 504                # Sync APProperty to property.Name505                try:506                    conn_sync = get_connection()507                    cur_sync = conn_sync.cursor()508                    cur_sync.execute(509                        'UPDATE accesspoint SET "APProperty" = %s '510                        'WHERE "UserID" = %s AND "PropID" = %s '511                        '  AND ( "APProperty" IS NULL OR btrim("APProperty") <> btrim(%s) )',512                        (prop_name, _uid(), int(prop_id), prop_name)513                    )514                    if cur_sync.rowcount:515                        print(f"[DEBUG] Synced accesspoint.APProperty rows: {cur_sync.rowcount} for PropID={prop_id}")516                    conn_sync.commit()517                except Exception as e:518                    try: conn_sync.rollback()519                    except: pass520                    print(f"❌ APProperty sync failed for PropID={prop_id}: {e}")521                finally:522                    try:523                        cur_sync.close(); conn_sync.close()524                    except: pass525 526                df_full, df_display = sync_accesspoints(prop_id)527                options = get_row_options(df_full)528 529                return (530                    gr.update(visible=True, value=df_display),531                    df_full,532                    prop_id,533                    gr.update(choices=options, value=None, interactive=not df_full.empty, visible=True),534                    gr.update(choices=options, value=None, interactive=not df_full.empty, visible=True),535                    gr.update(visible=False),536                    gr.update(visible=False, interactive=False),537                    gr.update(visible=True, interactive=True),538                    gr.update(visible=False),539                    gr.update(value="", visible=False),540                    gr.update(visible=False),541                    gr.update(visible=False),542                )543 544            def select_row(row_id, df):545                if not row_id or df is None or df.empty:546                    return (547                        "", "", False, "", None,548                        gr.update(choices=load_installer_options(), value=None),549                        "",550                        gr.update(visible=False),551                        gr.update(visible=False, interactive=False),552                        gr.update(interactive=True),553                        gr.update(interactive=False),554                        gr.update(visible=False),555                    )556                try:557                    selected_id = int(str(row_id))558                    match = df[df["ID"] == selected_id]559                except Exception:560                    match = pd.DataFrame()561 562                if match.empty:563                    return (564                        "", "", False, "", None,565                        gr.update(choices=load_installer_options(), value=None),566                        "",567                        gr.update(visible=False),568                        gr.update(visible=False, interactive=False),569                        gr.update(interactive=True),570                        gr.update(interactive=False),571                        gr.update(visible=False),572                    )573 574                row = match.iloc[0]575                opts = load_installer_options()576                ap_installer_id = row.get("APInstallerID")577                dd_value = "<to be appointed>"578                id_str = ""579                if pd.notna(ap_installer_id) and ap_installer_id is not None:580                    target = str(int(ap_installer_id))581                    for val in opts:582                        if "|" in val:583                            cid = val.split("|", 1)[0].strip()584                            if cid == target:585                                dd_value = val586                                id_str = target587                                break588                inout_val = row.get("ApInOut")589                inout_sel = str(inout_val) if isinstance(inout_val, str) and inout_val in ("In", "Out") else None590 591                return (592                    row["NameOfAccessPoint"],593                    gr.update(value=str(row["AccessPointID"]), interactive=False),594                    row["RestrictedAP"],595                    row["APDeviceName"],596                    inout_sel,597                    gr.update(choices=opts, value=dd_value),598                    id_str,599                    gr.update(visible=True),600                    gr.update(visible=True, interactive=True),601                    gr.update(interactive=False),602                    gr.update(interactive=False),603                    gr.update(visible=True),604                )605 606            def installer_changed(dd_value):607                if not dd_value:608                    return ""609                try:610                    cid, _ = str(dd_value).split("|", 1)611                    return str(int(cid))612                except Exception:613                    return ""614 615            def apply_edit(616                selected_row, df, new_name, new_phone, new_restrict, new_device,617                new_inout, dd_installer, ro_installer_id, prop_id618            ):619                if not selected_row or df is None or getattr(df, "empty", True):620                    return (621                        gr.update(), gr.update(), gr.update(),622                        "", "", False, "", None,623                        gr.update(choices=load_installer_options(), value=None), "",624                        gr.update(visible=False),625                        gr.update(visible=False),626                        gr.update(visible=False, interactive=False),627                        gr.update(interactive=True),628                        gr.update(interactive=False),629                        gr.update(visible=False),630                    )631                row_id = None632                sval = str(selected_row).strip()633                try:634                    row_id = int(sval)635                except Exception:636                    m = re.search(r"\((\d+)\)$", sval)637                    if m:638                        row_id = int(m.group(1))639                if row_id is None:640                    return (641                        gr.update(), gr.update(), gr.update(),642                        "", "", False, "", None,643                        gr.update(choices=load_installer_options(), value=None), "",644                        gr.update(visible=False),645                        gr.update(visible=False),646                        gr.update(visible=False, interactive=False),647                        gr.update(interactive=True),648                        gr.update(interactive=False),649                        gr.update(visible=False),650                    )651 652                sel_company_id = None653                sel_company = ""654                sentinel = "<to be appointed>"655                dd = (dd_installer or "").strip()656                if dd and dd != sentinel:657                    try:658                        cid, cname = dd.split("|", 1)659                        sel_company_id = int(cid.strip())660                        sel_company = cname.strip()661                    except Exception:662                        sel_company_id = None663                        sel_company = ""664                else:665                    sel_company_id = None666                    sel_company = ""667                    new_device = ""668 669                inout_value = new_inout if new_inout in ("In", "Out") else None670 671                conn = get_connection()672                cur = conn.cursor()673                try:674                    cur.execute(675                        'UPDATE accesspoint '676                        'SET "NameOfAccessPoint"=%s, '677                        '    "RestrictedAP"=%s, '678                        '    "APDeviceName"=%s, '679                        '    "ApInOut"=COALESCE(%s, "ApInOut"), '680                        '    "APInstaller"=%s, '681                        '    "APInstallerID"=%s '682                        'WHERE "ID"=%s',683                        (new_name, new_restrict, new_device, inout_value, sel_company, sel_company_id, row_id)684                    )685                    conn.commit()686                finally:687                    cur.close()688                    conn.close()689 690                df_updated, df_display = sync_accesspoints(prop_id)691                options = get_row_options(df_updated)692                return (693                    gr.update(value=df_display),694                    gr.update(choices=options, value=None),695                    gr.update(choices=options, value=None, interactive=not df_updated.empty),696                    "", "", False, "", None,697                    gr.update(choices=load_installer_options(), value=None),698                    "",699                    gr.update(visible=False),700                    gr.update(visible=False),701                    gr.update(visible=False, interactive=False),702                    gr.update(interactive=True),703                    gr.update(interactive=False),704                    gr.update(visible=False),705                )706 707            def cancel_edit():708                return (709                    "", "", False, "", None,710                    gr.update(choices=load_installer_options(), value=None),711                    "",712                    gr.update(visible=False),713                    gr.update(visible=False, interactive=False),714                    gr.update(interactive=True),715                    gr.update(interactive=False),716                    gr.update(visible=False),717                )718 719            def cancel_delete():720                return gr.update(value=None), gr.update(interactive=False, visible=False), gr.update(interactive=True), gr.update(visible=False)721 722            def add_row(prop_id):723                if not prop_id:724                    return pd.DataFrame(), gr.update(), gr.update(), gr.update(), None725                new_id = generate_id()726                new_access_point_id = generate_id()727                name = "<enter access point name>"728 729                ap_property_name = _get_property_name(prop_id, _uid())730                ap_installer_value = ""731                ap_installer_id = None732 733                conn = get_connection()734                cursor = conn.cursor()735                cursor.execute(736                    'INSERT INTO accesspoint '737                    '("ID","PropID","NameOfAccessPoint","AccessPointID","UserID","RestrictedAP","APDeviceName","APInstaller","APInstallerID","APProperty") '738                    'VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)',739                    (new_id, int(prop_id), name, int(new_access_point_id), _uid(), False, "", ap_installer_value, ap_installer_id, ap_property_name)740                )741                conn.commit()742                conn.close()743 744                # Generate & save PNG (real QR if lib present; placeholder otherwise)745                bdata = create_save_png_qrcode(str(new_access_point_id))746                pngfile = str(new_access_point_id) + ".png"747                with open(pngfile, "wb") as f:748                    f.write(bdata)749                print(f"QR image saved as {pngfile}")750 751                df_updated, df_display = sync_accesspoints(prop_id)752                options = get_row_options(df_updated)753                return (754                    df_display,755                    gr.update(choices=options, value=None),756                    gr.update(choices=options, value=None),757                    gr.update(choices=options, value=None),758                    df_updated759                )760 761            def enable_delete_button(row_id):762                return gr.update(interactive=bool(row_id), visible=bool(row_id)), gr.update(visible=bool(row_id))763 764            def delete_row(row_id, df, prop_id):765                if not row_id or df is None or df.empty:766                    return df, gr.update(), gr.update(), gr.update(interactive=False, visible=False), gr.update(visible=False), df767                try:768                    selected_id = int(str(row_id))769                    match = df[df["ID"] == selected_id]770                except Exception:771                    match = pd.DataFrame()772                if match.empty:773                    return df, gr.update(), gr.update(), gr.update(interactive=False, visible=False), gr.update(visible=False), df774 775                row_id_val = int(match.iloc[0]["ID"])776                conn = get_connection()777                cursor = conn.cursor()778                cursor.execute('DELETE FROM accesspoint WHERE "ID" = %s', (row_id_val,))779                conn.commit()780                conn.close()781                df_updated, df_display = sync_accesspoints(prop_id)782                options = get_row_options(df_updated)783                return df_display, gr.update(choices=options, value=None), gr.update(choices=options, value=None), gr.update(interactive=False, visible=False), gr.update(visible=False), df_updated784 785            # ---- small UI helpers for chaining ----786            def _activate_selects_if_options(df):787                has_rows = (df is not None) and (not getattr(df, "empty", True))788                return (789                    gr.update(interactive=has_rows),790                    gr.update(interactive=has_rows),791                )792 793            def _sync_delete_interactive_on_edit(selected_val):794                return gr.update(interactive=not bool(selected_val))795 796            def _enable_selectors():797                return gr.update(interactive=True), gr.update(interactive=True)798 799            def _enable_selectors_and_add():800                return gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=True)801 802            def _disable_both_selectors():803                return gr.update(interactive=False), gr.update(interactive=False)804 805            def _toggle_edit_and_add_on_delete(row_choice):806                active = bool(row_choice)807                if active:808                    return gr.update(interactive=False), gr.update(interactive=False)809                else:810                    return gr.update(), gr.update()811 812            def _refresh_table_and_both_selects(prop_id):813                if not prop_id:814                    return gr.update(), None, gr.update(), gr.update()815                df_full, df_display = sync_accesspoints(prop_id)816                options = get_row_options(df_full)817                has_rows = (df_full is not None) and (not getattr(df_full, "empty", True))818                return (819                    gr.update(value=df_display),820                    df_full,821                    gr.update(choices=options, value=None, interactive=has_rows),822                    gr.update(choices=options, value=None, interactive=has_rows),823                )824 825            # ---- UI wiring ----826            prop_map: Dict[str, tuple[int | None, str, str]] = {}827            print("DEBUG (prime): starting with UID =", _uid())828 829            # Start empty; demo.load populates it830            prop_dropdown = gr.Dropdown(label="Select Property (Owner)", choices=[], value=None)831            832            btn_reload_props = gr.Button("Reload Properties", variant="secondary")833            btn_reload_props.click(_prime_uid_then_fill_properties, outputs=[prop_dropdown])834            835            accesspoint_table = gr.Dataframe(label="Tenant Users", visible=False, interactive=False)836 837            state_df = gr.State()838            state_prop_id = gr.State()839 840            demo.load(_prime_uid_then_fill_properties, inputs=[], outputs=[prop_dropdown])841 842            with gr.Row():843                add_btn = gr.Button("Add Row", interactive=False)844 845            with gr.Row():846                row_to_edit = gr.Dropdown(label="Select Row to Edit", choices=[], value=None, interactive=False)847                row_to_delete = gr.Dropdown(label="Select Row to Delete", choices=[], value=None, interactive=False)848 849            with gr.Row():850                delete_btn = gr.Button("Apply Delete", interactive=False, visible=False)851                cancel_delete_btn = gr.Button("Cancel Delete", visible=False)852 853            with gr.Group(visible=False) as edit_group:854                gr.Markdown("""<div style="background-color:#666666; color:#FFFFFF; padding:10px; border-radius:0px;">Access Point Details</div>""")855                with gr.Row():856                    edit_phone = gr.Textbox(label="Access Point ID (auto)", interactive=False)857                    edit_name = gr.Textbox(label="Name of Access Point")858                    edit_inout = gr.Dropdown(label="Direction (In/Out)", choices=["In","Out"], value=None)859                    edit_restrict = gr.Checkbox(label="Tick if Restricted AP")860                gr.Markdown("""<div style="background-color:#666666; color:#FFFFFF; padding:10px; border-radius:0px;">Access Point Installer details</div>""")861                with gr.Row():862                    edit_installer_dd = gr.Dropdown(863                        label="Installer (select company)",864                        choices=load_installer_options(),865                        value=None866                    )867                    edit_installer_id = gr.Textbox(label="Installer ID (auto)", interactive=False)868                    edit_device = gr.Textbox(label="Installer's Device Name (auto))", interactive=False)869                with gr.Row():870                    error_msg = gr.Markdown(visible=False)871 872            with gr.Row():873                apply_edit_btn = gr.Button("Apply Edit", interactive=False, visible=False)874                cancel_edit_btn = gr.Button("Cancel Edit", visible=False)875 876            # events877            prop_dropdown.change(878                select_property,879                inputs=prop_dropdown,880                outputs=[881                    accesspoint_table, state_df, state_prop_id,882                    row_to_edit, row_to_delete,883                    apply_edit_btn, delete_btn, add_btn,884                    edit_group, error_msg, cancel_edit_btn, cancel_delete_btn885                ],886            ).then(887                _activate_selects_if_options,888                inputs=[state_df],889                outputs=[row_to_edit, row_to_delete],890            )891 892            edit_installer_dd.change(installer_changed, inputs=edit_installer_dd, outputs=edit_installer_id)893 894            row_to_edit.change(895                select_row,896                inputs=[row_to_edit, state_df],897                outputs=[898                    edit_name, edit_phone, edit_restrict, edit_device, edit_inout,899                    edit_installer_dd, edit_installer_id,900                    edit_group, apply_edit_btn, add_btn, delete_btn, cancel_edit_btn901                ]902            ).then(903                _sync_delete_interactive_on_edit,904                inputs=[row_to_edit],905                outputs=[row_to_delete]906            )907 908            cancel_edit_btn.click(909                cancel_edit,910                outputs=[edit_name, edit_phone, edit_restrict, edit_device, edit_inout,911                         edit_installer_dd, edit_installer_id, edit_group, apply_edit_btn,912                         add_btn, delete_btn, cancel_edit_btn]913            ).then(914                _refresh_table_and_both_selects,915                inputs=[state_prop_id],916                outputs=[accesspoint_table, state_df, row_to_edit, row_to_delete]917            ).then(918                _enable_selectors, inputs=[], outputs=[row_to_edit, row_to_delete]919            )920 921            row_to_delete.change(922                enable_delete_button, inputs=row_to_delete, outputs=[delete_btn, cancel_delete_btn]923            ).then(924                _toggle_edit_and_add_on_delete, inputs=[row_to_delete], outputs=[row_to_edit, add_btn]925            )926 927            cancel_delete_btn.click(928                cancel_delete,929                outputs=[row_to_delete, delete_btn, add_btn, cancel_delete_btn]930            ).then(931                _enable_selectors, inputs=[], outputs=[row_to_edit, row_to_delete]932            )933 934            apply_edit_btn.click(935                apply_edit,936                inputs=[row_to_edit, state_df, edit_name, edit_phone, edit_restrict, edit_device,937                        edit_inout, edit_installer_dd, edit_installer_id, state_prop_id],938                outputs=[accesspoint_table, row_to_edit, row_to_delete,939                         edit_name, edit_phone, edit_restrict, edit_device, edit_inout,940                         edit_installer_dd, edit_installer_id, edit_group, error_msg,941                         apply_edit_btn, add_btn, delete_btn, cancel_edit_btn]942            ).then(943                _refresh_table_and_both_selects,944                inputs=[state_prop_id],945                outputs=[accesspoint_table, state_df, row_to_edit, row_to_delete]946            ).then(947                _enable_selectors, inputs=[], outputs=[row_to_edit, row_to_delete]948            )949 950            add_btn.click(951                _disable_both_selectors, inputs=[], outputs=[row_to_edit, row_to_delete]952            ).then(953                add_row, inputs=state_prop_id,954                outputs=[accesspoint_table, row_to_edit, row_to_delete, row_to_edit, state_df]955            ).then(956                _enable_selectors, inputs=[], outputs=[row_to_edit, row_to_delete]957            )958 959            delete_btn.click(960                delete_row, inputs=[row_to_delete, state_df, state_prop_id],961                outputs=[accesspoint_table, row_to_edit, row_to_delete, delete_btn, cancel_delete_btn, state_df]962            ).then(963                _enable_selectors_and_add, inputs=[], outputs=[row_to_edit, row_to_delete, add_btn]964            )965 966    return demo967 968# app = launch_accesspoints_module()969# app.launch(share=True)970 971 972