import streamlit as st import streamlit_authenticator as stauth from pathlib import Path import sys import pandas as pd import subprocess from datetime import datetime import os from huggingface_upload import upload_all_to_huggingface # Allow imports of project modules sys.path.insert(0, str(Path(__file__).parent.parent)) from user_management import HuggingFaceUserManager, load_user_config st.set_page_config(page_title="Admin Panel", layout="wide", page_icon="🛠️") # CSS st.markdown(""" """, unsafe_allow_html=True) # CONFIG config, using_hf = load_user_config() if config is None: st.error("Authentication configuration not found!") st.stop() # AUTH SYSTEM authenticator = stauth.Authenticate( config['credentials'], config['cookie']['name'], config['cookie']['key'], config['cookie']['expiry_days'] ) try: authenticator.login('main') except Exception as e: st.error(f"Login error: {e}") name = st.session_state.get("name") authentication_status = st.session_state.get("authentication_status") username = st.session_state.get("username") if authentication_status == False: st.error('Username/password is incorrect') st.stop() if authentication_status == None: st.warning('Please enter your username and password') st.stop() # AUTH VIEW if authentication_status: with st.sidebar: st.markdown("---") st.markdown(f"**Logged in as:** {name}") st.markdown(f"**Username:** {username}") authenticator.logout('Logout', 'sidebar') ALLOWED_USERNAMES = set(config['credentials']['usernames'].keys()) if username not in ALLOWED_USERNAMES: st.error(f"User '{username}' is not authorized.") st.stop() # HEADER st.success(f"Welcome, {name}!") st.markdown("---") st.markdown("""

Admin Panel

Cloud data sync controls

""", unsafe_allow_html=True) st.markdown("---") # Tabs tab1, tab2, tab3 = st.tabs(["Dashboard", "Data Pipeline", "User Management"]) # ------------------------------------------------------------------ # TAB 1 — Dashboard # ------------------------------------------------------------------ with tab1: st.subheader("Admin Dashboard") users = config['credentials']['usernames'] admin_data = [ { "Username": uname, "Name": data.get("name"), "Email": data.get("email"), "Current User": "Admin" if uname == username else "" } for uname, data in users.items() ] st.dataframe(pd.DataFrame(admin_data), width="stretch", hide_index=True) # ------------------------------------------------------------------ # TAB 2 — DATA PIPELINE # ------------------------------------------------------------------ with tab2: st.subheader("Data Pipeline") if 'huggingface' not in st.secrets: st.warning("Add HuggingFace credentials to `.streamlit/secrets.toml`") st.stop() from huggingface_upload import upload_to_huggingface, test_hf_connection # --- Connection Test st.markdown("Connection Status") col1, col2 = st.columns(2) with col1: if st.button("Test HuggingFace Connection", width='stretch'): ok, msg = test_hf_connection() (st.success if ok else st.error)(msg) with col2: repo = st.secrets["huggingface"]["dataset_repo"] st.info(f"Dataset: {repo}") st.markdown("---") # --- Full Data Update Section st.subheader("Full Data Update") st.info("Pull new data, process PDFs, generate embeddings, and upload to HuggingFace.") # ➤ NEW UI CONTROL — Pull new data? pull_new_data = st.radio( "Pull new data from LegiScan?", options=[ ("no", "No - Use existing local data"), ("yes", "Yes - Pull fresh data (costs API quota)"), ], format_func=lambda x: x[1], index=0, key="pull_option" ) # ➤ NEW UI CONTROL — overwrite known_bills.json? overwrite_pdf = st.radio( "After fixing PDF bills, overwrite data/known_bills.json?", options=[ ("no", "No - keep original file"), ("yes", "Yes - overwrite with cleaned PDF text"), ], format_func=lambda x: x[1], index=0, key="overwrite_option" ) # Run full update if st.button("Run Full Update & Upload", type="primary", width='stretch'): status_container = st.container() with status_container: st.markdown("### Step 1: Running Data Pipeline") with st.status("Processing data...", expanded=True) as status: try: update_cmd = [sys.executable, "update_data.py"] legiscan_answer = "y\n" if pull_new_data[0] == "yes" else "n\n" import os from dotenv import load_dotenv load_dotenv() env = os.environ.copy() # Pass OpenAI keys (existing logic) openai_key = ( st.secrets.get("openai_api_key") or st.secrets.get("OPENAI_API_KEY") or env.get("openai_api_key") or env.get("OPENAI_API_KEY") ) if openai_key: env["OPENAI_API_KEY"] = openai_key env["openai_api_key"] = openai_key st.success("OpenAI key found") else: st.warning("OpenAI API key missing!") # ➤ NEW: Pass PDF overwrite decision into environment env["FIX_PDF_OVERWRITE"] = ( "yes" if overwrite_pdf[0] == "yes" else "no" ) log_file = Path("pipeline_last_run.log") with log_file.open("w", encoding="utf-8") as lf: proc = subprocess.Popen( update_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, text=True, bufsize=1, env=env, ) # Send LegiScan yes/no try: proc.stdin.write(legiscan_answer) proc.stdin.write("n\n") # continue-on-error prompt proc.stdin.flush() proc.stdin.close() except: pass # Stream output for line in proc.stdout: line = line.rstrip("\n") st.text(line) lf.write(line + "\n") rc = proc.wait() if rc == 0: status.update(label="Data pipeline completed", state="complete") st.success("Processing successful!") st.markdown("---") st.markdown("### Step 2: Uploading to HuggingFace") with st.spinner("Uploading..."): url = upload_to_huggingface() st.success("Uploaded to HuggingFace!") st.code(url) st.cache_data.clear() else: status.update(label="Pipeline failed", state="error") st.error(f"Pipeline exited with code {rc}") except Exception as e: st.error(f"Pipeline error: {e}") st.exception(e) st.markdown("---") with st.expander("Manual Upload Only"): st.info("Use this only when skipping update_data.py") if st.button("Upload Existing Data", width='stretch'): with st.spinner("Uploading..."): url = upload_to_huggingface() st.success("Uploaded!") st.code(url) with tab3: st.subheader("User Management") if using_hf: st.success("Using HuggingFace for persistent user storage") try: user_manager = HuggingFaceUserManager() st.markdown("Add New Admin") with st.form("add_user_form"): col1, col2 = st.columns(2) with col1: new_username = st.text_input("Username", key="new_username") new_email = st.text_input("Email", key="new_email") with col2: new_name = st.text_input("Full Name", key="new_name") new_password = st.text_input("Password", type="password", key="new_password") submit_add = st.form_submit_button("Add Admin", type="primary", width='stretch') if submit_add: if not all([new_username, new_email, new_name, new_password]): st.error("Please fill in all fields") else: with st.spinner("Adding user..."): import bcrypt hashed_password = bcrypt.hashpw(new_password.encode(), bcrypt.gensalt()).decode() success, message, commit_url = user_manager.add_user( new_username, new_email, new_name, hashed_password ) if success: st.success(f"{message}") st.cache_data.clear() if commit_url: with st.expander("View commit"): st.code(commit_url) st.rerun() else: st.error(f"{message}") st.markdown("---") st.markdown("Edit Admin") users = config['credentials']['usernames'] usernames_list = list(users.keys()) with st.form("edit_user_form"): user_to_edit = st.selectbox( "Select user to edit", options=usernames_list, key="edit_username" ) current_user = users.get(user_to_edit, {}) st.markdown("**Current Details:**") st.text(f"Email: {current_user.get('email', 'N/A')}") st.text(f"Name: {current_user.get('name', 'N/A')}") st.markdown("**New Details** (leave blank to keep current):") col1, col2 = st.columns(2) with col1: new_email = st.text_input("New Email", key="edit_email", placeholder="Leave blank to keep current") new_password = st.text_input("New Password", type="password", key="edit_password", placeholder="Leave blank to keep current") with col2: new_name = st.text_input("New Name", key="edit_name", placeholder="Leave blank to keep current") submit_edit = st.form_submit_button("Update Admin", type="primary", width='stretch') if submit_edit: if not any([new_email, new_name, new_password]): st.warning("Please enter at least one field to update") else: with st.spinner("Updating user..."): hashed_password = None if new_password: import bcrypt hashed_password = bcrypt.hashpw(new_password.encode(), bcrypt.gensalt()).decode() success, message, commit_url = user_manager.update_user( user_to_edit, new_email=new_email if new_email else None, new_name=new_name if new_name else None, new_password=hashed_password ) if success: st.success(f"{message}") st.info("Refreshing user data...") st.cache_data.clear() if commit_url: with st.expander("View commit"): st.code(commit_url) st.info("Please log out and log back in if you changed your own password") st.rerun() else: st.error(f"{message}") st.markdown("---") # Remove user st.markdown("Remove Admin") users = config['credentials']['usernames'] usernames_list = list(users.keys()) if len(usernames_list) > 1: with st.form("remove_user_form"): user_to_remove = st.selectbox( "Select user to remove", options=usernames_list, key="remove_username" ) st.warning(f"This will permanently delete user: **{user_to_remove}**") confirm_remove = st.checkbox("I confirm I want to remove this user") submit_remove = st.form_submit_button("Remove Admin", type="secondary", width='stretch') if submit_remove: if not confirm_remove: st.error("Please confirm the removal") elif user_to_remove == username: st.error("You cannot remove yourself!") else: with st.spinner("Removing user..."): success, message, commit_url = user_manager.remove_user(user_to_remove) if success: st.success(f"✅ {message}") st.cache_data.clear() if commit_url: with st.expander("View commit"): st.code(commit_url) st.rerun() else: st.error(f"{message}") else: st.info("ℹCannot remove the last admin user") st.markdown("---") # Show current users st.markdown("Current Admins") for uname, udata in users.items(): with st.expander(f"{udata.get('name', uname)} (@{uname})"): st.write(f"**Email:** {udata.get('email', 'N/A')}") st.write(f"**Username:** {uname}") st.write(f"**Admin Status:**Admin") if uname == username: st.info("This is you!") except Exception as e: st.error(f"Error initializing user manager: {e}") st.exception(e) else: st.warning("Using secrets.toml (read-only)") st.info("For persistent user management, add HuggingFace credentials to secrets.toml") with st.expander("How to add users manually"): st.markdown(""" **To add new users when using secrets.toml:** 1. **Generate password hash:** ```bash python generate_password_hash.py ``` 2. **Add to secrets.toml:** ```toml [auth.credentials.usernames.newuser] email = "user@vanderbilt.edu" name = "New User" password = "$2b$12$HASH_FROM_STEP_1" ``` 3. **Update on HuggingFace Spaces** (re-upload secrets.toml) All registered users automatically get admin access. """) st.markdown("---") st.markdown("Current Admins") if 'credentials' in config and 'usernames' in config['credentials']: users = config['credentials']['usernames'] for uname, udata in users.items(): with st.expander(f"{udata.get('name', uname)} (@{uname})"): st.write(f"**Email:** {udata.get('email', 'N/A')}") st.write(f"**Username:** {uname}") st.write(f"**Admin Status:Admin")