CoolFace
Apppublic

JonoRens/gr_test_app

sourceHugging Faceupdated 11mo agoView on Hugging Face
0likes
app.py473 linesDownload Raw Back to root
1# app.py2 3 4# ========== IMPORTS ==========5import os6import pickle7from pathlib import Path8import gradio as gr9import psycopg210import psycopg2.extras  # for RealDictCursor (named dict rows)11import json12import ast13 14# Encryption for DB config + active-user pickle15try:16    from cryptography.fernet import Fernet, InvalidToken17except ImportError as e:18    raise ImportError(19        "cryptography is required. Install with: pip install cryptography"20    ) from e21 22# ======== MODULE IMPORTS ========23from property_module_hf import launch_property_module24from parkingbays_module_hf import launch_parkingbays_module25from permissions_module_hf import launch_permissions_module26from accesspoints_module_hf import launch_accesspoints_module27from intercom_module_hf import launch_intercom_module28from installers_module_hf import launch_installers_module29 30# ========== DB CONNECTION ==========31 32APP_DIR = Path(__file__).parent33 34def decrypt_nhost() -> dict:35    key = os.getenv("SECRET_KEY", "").strip()36    if not key:37        raise RuntimeError("SECRET_KEY not set in this Space (Settings → Secrets).")38 39    enc_path = APP_DIR / "nhost_params.enc"40    if not enc_path.exists():41        raise RuntimeError("Encrypted DB config (nhost_params.enc) not found in repo.")42 43    token = enc_path.read_bytes()44    decrypted = Fernet(key.encode("utf-8")).decrypt(token).decode("utf-8")45 46    # Prefer JSON; allow Python-literal fallback47    try:48        params = json.loads(decrypted)49    except json.JSONDecodeError:50        params = ast.literal_eval(decrypted)  # handles "{'host': ...}" format51 52    # Optional: normalize port to int53    if "port" in params:54        try:55            params["port"] = int(params["port"])56        except Exception:57            pass58 59    if not isinstance(params, dict):60        raise TypeError("Decrypted nhost config is not a dict.")61 62    return params63 64 65def get_connection():66    params = decrypt_nhost()67    if isinstance(params, str):68        raise TypeError("nhost params is a string; expected dict. Do not json.dumps() the config.")69    return psycopg2.connect(**params)  # ← use params, not another decrypt_nhost()70 71 72#===========================EVERYTHING BELOW GETS COPIED TO APP.PY73 74# ========== SETTINGS ==========75PropUser1 = None  # start empty; user must enter a valid ID76HERE = Path(__file__).resolve().parent77KEY_FILE = HERE / "openqr_secret.key"78DATA_FILE = HERE / "openqr_active_user.pkl.enc"79 80# ========== STYLES ==========81TAB_CSS = """82body,83.gradio-container,84.gradio-container .tabs,85.gradio-container .tab-nav,86.gradio-container .tabitem {87  background: #ffffff !important;88  box-shadow: none !important;89}90 91/* Load user button color */92#load-user-btn {93  background: #374628 !important;94  border-color: #374628 !important;95  color: #ffffff !important;96}97#load-user-btn:hover {98  filter: brightness(0.95);99}100"""101 102#=========== GET USER ID103def _ctx_get_user_id():104    """105    Returns the active Manager User ID set by the Load User flow.106    We treat '0' and empty as 'unset' so modules don't prematurely try to load data.107    """108    v = os.getenv("OPENQR_ACTIVE_USER_ID", "").strip()109    try:110        n = int(v) if v else None111        return None if (n in (None, 0)) else n112    except Exception:113        return None114 115# ========== ENCRYPTED FILE HELPERS ==========116def _ensure_key() -> bytes:117    if KEY_FILE.exists():118        return KEY_FILE.read_bytes()119    key = Fernet.generate_key()120    KEY_FILE.write_bytes(key)121    return key122 123def _write_encrypted_pickle(data: dict):124    key = _ensure_key()125    f = Fernet(key)126    blob = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)127    DATA_FILE.write_bytes(f.encrypt(blob))128 129def _read_encrypted_pickle() -> dict | None:130    """131    Returns dict with (possibly stale) ActiveUserID/Name/CoName, or None on failure.132    We only use the ID; name/company are refreshed from DB.133    """134    try:135        key = KEY_FILE.read_bytes()136        f = Fernet(key)137        blob = DATA_FILE.read_bytes()138        data = pickle.loads(f.decrypt(blob))139        return data if isinstance(data, dict) else None140    except (FileNotFoundError, InvalidToken, OSError, ValueError) as e:141        print(f"_read_encrypted_pickle() warning: {e}")142        return None143 144# ========== FIELD EXTRACTION HELPERS ==========145_COMPANY_KEYS = (146    "UserCompanyName",      # ← add this (likely your actual column)147    "UserCompany",          # ← optional common variant148    "MUserCompanyName",149    "MUserCoName",150    "MUserCompany",151    "CompanyName",152    "Company",153    "MUserCo",154)155 156 157def _extract_user_fields(row: dict):158    """159    Accepts a dict row from managerusers (RealDictCursor).160    Returns: ActiveUserID, ActiveUserName, ActiveUserCoName, IsActive, IsInstaller161    """162    uid = row.get("MUserID")163    name = row.get("MUserName") or ""164    company = ""165    for k in _COMPANY_KEYS:166        if k in row and row[k] is not None:167            v = str(row[k]).strip()168            if v != "":169                company = v170                break171 172    is_active = bool(row.get("MUserActive") or row.get("Active") or row.get("IsActive") or False)173    installers = bool(row.get("Installers", False))  # default False if col missing174    uid = int(uid) if uid is not None else None175    return uid, name, company, is_active, installers176 177# ========== HELP MODULES ==========178def launch_help_module():179    with gr.Blocks() as help_app:180        with gr.Tabs():181            with gr.TabItem("Overview"):182                gr.Markdown("◉ **RyGo Desktop** is a desktop app for property managers")183                gr.Markdown("◉ The app has 5 modules: **Property**, **Parking Bays**, **Permissions**, **Intercoms** and **Access Points**")184                gr.Markdown("**To be developed:**")185                gr.Markdown("   ▷ manager sign-up")186                gr.Markdown("   ▷ when change / delete a LeaseID in parking bays, ensure all tenant users are marked as inactive")187                gr.Markdown("   ▷ when change / delete a BayNo in parking bays, ensure allocated bays are cleared from tenant users")188                gr.Markdown("   ▷ add intercom report to property")189                gr.Markdown("   ▷ add report identifying tenant users who have access to a particular access point")190                gr.Markdown("   ▷ add a PDF column in property table for bay drawings; provide upload/download")191                gr.Markdown("   ▷ add licence plate to tenant user & include in parking bays report")192                gr.Markdown("   ▷ export reports to Excel")193 194            with gr.TabItem("Properties"):195                gr.Markdown("◉ Manage properties (add, edit, delete) and property-related reports.")196                gr.Markdown("◉ Reports include:")197                gr.Markdown("   ▷ Bay summary (loaded / let / allocated)")198                gr.Markdown("   ▷ All parking bays at a property with details of lets & allocations")199                gr.Markdown("   ▷ List of intercoms")200                gr.Markdown("   ▷ List of access points (AP)")201                gr.Markdown("   ▷ Individual activity report")202 203 204            with gr.TabItem("Permissions"):205                gr.Markdown("◉ Manage tenant users connected to a property (add, edit, delete).")206                gr.Markdown("◉ Enter tenant's name & mobile, connect to lease, bay, and other access points.")207 208            with gr.TabItem("Access Points"):209                gr.Markdown("◉ Manage access points at a property, set Unrestricted/Restricted (permissions required if Restricted).")210 211#            with gr.TabItem("Intercoms"):212#                gr.Markdown("◉ Intercoms module overview (coming features).")213 214 215            with gr.TabItem("ays & Leases"):216                gr.Markdown("◉ Manage parking bays at a property (add, edit, delete).")217                gr.Markdown("◉ Assign bays to tenants via Lease ID and connect restricted access points.")218 219 220    return help_app221 222def launch_installer_help_module():223    with gr.Blocks() as help_app:224        with gr.Tabs():225            with gr.TabItem("Installer overview"):226                gr.Markdown("◉ The Installer app gives the installer access to all Access Points (AP) allocated to the installer across properties")227                gr.Markdown("◉ Basic steps:")228                gr.Markdown("1) Choose Property tab.\n"229                            "2) Choose row to edit.\n"230                            "3) Insert / edit Name of device at AP, the location of access point and the API for the AP.\n"231                            "4) Return to main menu when done.")232    return help_app233 234# ========== MAIN APP ==========235def main_app():236 237    # Ensure no stale value at process start; user must enter a UserID each run.238    os.environ.pop("OPENQR_ACTIVE_USER_ID", None)239 240    with gr.Blocks() as app:241        # Global CSS242        gr.HTML(f"<style>{TAB_CSS}</style>")243 244        # Header + status245        header_md = gr.Markdown('<div style="color:#374628;"><h2 style="margin:0;">RyGo Admin Dashboard</h2></div>')246        status_md = gr.Markdown("", visible=False)247 248        # User input row249        with gr.Row():250            muser_in = gr.Number(label="Manager User ID", precision=0, value=PropUser1)251        with gr.Row():252            load_btn = gr.Button("Load user", variant="primary", elem_id="load-user-btn")253 254        # --- Active User ID (edit step, shown only when an ID already exists) ---255        with gr.Row():256            curr_uid_md = gr.Markdown("")  # shows "Current Active User ID: `...`" when present257        with gr.Row():258            edit_uid_tb = gr.Textbox(label="Active User ID (edit if needed)", value="", visible=False)259        with gr.Row():260            confirm_uid_btn = gr.Button("Enter / Edit User ID", variant="secondary", visible=False)261        with gr.Row():262            uid_note_md = gr.Markdown("")263 264        # App-wide state (kept for compatibility)265        s_user_id = gr.State(None)266        s_user_name = gr.State("")267        s_user_coname = gr.State("")268 269        # --- init current-id display & prefill edit box ---270        def _init_uid_display(uid):271            has_uid = uid not in (None, "")272            md_text = f"**Current Active User ID:** `{uid}`" if has_uid else ""273            return (274                md_text,275                gr.update(value=(uid if has_uid else ""), visible=has_uid),  # edit_uid_tb276                gr.update(visible=has_uid),                                   # confirm_uid_btn277                gr.update(value="")                                           # uid_note_md cleared278            )279 280        # ===== Standard Manager Tabs (hidden until user is validated) =====281        tabs_group = gr.Group(visible=False)282        with tabs_group:283            with gr.Tabs():284 285 286                with gr.TabItem("🏢 Property"):287                    prop_ui = launch_property_module(get_user_id=_ctx_get_user_id)288 289                with gr.TabItem("👷‍ Permissions"):290                    tenant_ui = launch_permissions_module(get_user_id=_ctx_get_user_id)291 292                with gr.TabItem("🚥 Access Points"):293                    access_ui = launch_accesspoints_module(get_user_id=_ctx_get_user_id)294 295                with gr.TabItem("📱 Intercoms"):296                    intercom_ui = launch_intercom_module(get_user_id=_ctx_get_user_id)297 298                with gr.TabItem("🅿️ Bays & Leases"):299                   bays_ui = launch_parkingbays_module(get_user_id=_ctx_get_user_id)300 301 302                with gr.TabItem("Help"):303                    _ = launch_help_module()304 305        # ===== Installer Tabs (hidden until user is validated and flagged as installer) =====306        installer_tabs_group = gr.Group(visible=False)307        with installer_tabs_group:308            with gr.Tabs():309                with gr.TabItem("Installer's APs"):310                    # IMPORTANT: pass the get_user_id callback so the module can resolve after load311                    installers_ui = launch_installers_module(get_user_id=_ctx_get_user_id)312                with gr.TabItem("Installer help"):313                    _ = launch_installer_help_module()314 315        # ===== Load user handler (manual flow) =====316        def load_user(muser_id):317            # 1) Validate numeric input318            try:319                muser_id = int(muser_id)320            except (TypeError, ValueError):321                return (322                    gr.update(value='<div style="color:#374628;"><h2 style="margin:0;">RyGo Admin Dashboard</h2></div>\n⚠️ Enter a valid numeric Manager User ID.'),323                    gr.update(value="", visible=True),324                    gr.update(visible=False),  # tabs_group325                    gr.update(visible=False),  # installer_tabs_group326                    None, "", "",327                    gr.update(),         # keep input328                    gr.update()          # keep button329                )330 331            # 2) Check user existence & active status using named dict rows332            with get_connection() as conn:333                with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:334                    cur.execute(335                        'SELECT * FROM managerusers WHERE "MUserID" = %s',336                        (muser_id,),337                    )338                    row = cur.fetchone()339 340            if not row:341                return (342                    gr.update(value='<div style="color:#374628;"><h2 style="margin:0;">RyGo Admin Dashboard</h2></div>\nManager user does not exist. Please enter a correct User ID.'),343                    gr.update(value="", visible=True),344                    gr.update(visible=False),345                    gr.update(visible=False),346                    None, "", "",347                    gr.update(), gr.update()348                )349 350            ActiveUserID, ActiveUserName, ActiveUserCoName, is_active, is_installer = _extract_user_fields(row)351 352            if not is_active:353                return (354                    gr.update(value='<div style="color:#374628;"><h2 style="margin:0;">RyGo Admin Dashboard</h2></div>\nManager user exists but is not active.'),355                    gr.update(value="", visible=True),356                    gr.update(visible=False),357                    gr.update(visible=False),358                    None, "", "",359                    gr.update(), gr.update()360                )361 362            # 3) Success → write encrypted pickle, set env var, show appropriate tabs, clear+hide inputs363            try:364                _write_encrypted_pickle(365                    {366                        "ActiveUserID": ActiveUserID,367                        "ActiveUserName": ActiveUserName,368                        "ActiveUserCoName": ActiveUserCoName,369                        "Installers": is_installer,370                    }371                )372            except Exception as e:373                print(f"WARNING: failed to write encrypted active user file: {e}")374 375            if ActiveUserID is not None:376                os.environ["OPENQR_ACTIVE_USER_ID"] = str(ActiveUserID)  # cached for modules377 378            header = (379                '<div style="color:#374628;"><h2 style="margin:0;">RyGo Admin Dashboard</h2></div>\n'380                f"**User:** {ActiveUserName}  (ID: {ActiveUserID} )&nbsp;&nbsp; **Company:** {ActiveUserCoName}"381            )382 383            # Toggle groups per Installers flag384            show_manager = not is_installer385            show_installer = is_installer386 387            return (388                gr.update(value=header),                 # header_md389                gr.update(value="", visible=False),      # status_md hidden to remove gap390                gr.update(visible=show_manager),         # tabs_group391                gr.update(visible=show_installer),       # installer_tabs_group392                ActiveUserID, ActiveUserName, ActiveUserCoName,393                gr.update(value=None, visible=False),    # muser_in → clear+hide394                gr.update(visible=False),                # load_btn → hide395            )396 397        # Wire the button (NOTE: +1 output for installer_tabs_group)398        load_btn.click(399            load_user,400            inputs=[muser_in],401            outputs=[402                header_md, status_md,403                tabs_group, installer_tabs_group,  # both groups404                s_user_id, s_user_name, s_user_coname,405                muser_in, load_btn406            ]407        )408 409        # --- confirm & check flow (reuse the existing load_user pipeline) ---410        def _set_muser_in_from_text(txt_value):411            try:412                return int(str(txt_value).strip())413            except Exception:414                return None415 416        def _post_check_ui(s_uid):417            """418            After calling load_user(), decide whether to show an error note or hide the edit UI.419            - If s_user_id is None/empty → user doesn't exist: show note and keep input/button visible.420            - If s_user_id is valid → user exists: clear note, hide input + button.421            """422            if s_uid in (None, ""):423                return (424                    "User ID does not exist - try again",425                    gr.update(visible=True),   # edit_uid_tb stays visible for retry426                    gr.update(visible=True)    # confirm button stays visible427                )428            else:429                return (430                    "",431                    gr.update(value="", visible=False),  # hide & clear input432                    gr.update(visible=False)             # hide button433                )434 435        confirm_uid_btn.click(436            _set_muser_in_from_text,437            inputs=[edit_uid_tb],438            outputs=[muser_in],439        ).then(440            load_user,441            inputs=[muser_in],442            outputs=[443                header_md, status_md,444                tabs_group, installer_tabs_group,445                s_user_id, s_user_name, s_user_coname,446                muser_in, load_btn447            ]448        ).then(449            _post_check_ui,450            inputs=[s_user_id],451            outputs=[uid_note_md, edit_uid_tb, confirm_uid_btn],452        )453 454        # We intentionally do not auto-load any saved user on startup:455        # user must enter a UserID each run per your (a)-(e) flow.456        app.load(457            _init_uid_display,458            inputs=[s_user_id],459            outputs=[curr_uid_md, edit_uid_tb, confirm_uid_btn, uid_note_md],460        )461 462    # IMPORTANT: return the Blocks object so app is not None463    return app464 465# ========== ENTRY POINT ==========466if __name__ == "__main__":467    app = main_app()468    app.launch()469    # app.launch(server_name="127.0.0.1", server_port=7860, inbrowser=True, show_error=True)470 471 472 473