Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
| 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(""" | |
| <style> | |
| .main .block-container { padding-top: 2rem; max-width: 1200px; } | |
| h2 { color: #e0e0e0 !important; font-weight: 400 !important; font-size: 1.5rem !important; } | |
| </style> | |
| """, 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(""" | |
| <div style='text-align: center; padding: 1rem 0 2rem 0;'> | |
| <h1 style='color: #1f2937;'>Admin Panel</h1> | |
| <p style='color: #6b7280;'>Cloud data sync controls</p> | |
| </div> | |
| """, 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") | |