From fbefffe00254ff7ef55cb6e2124cd3dc8c62010d Mon Sep 17 00:00:00 2001 From: DJP Date: Wed, 1 Oct 2025 19:10:51 -0400 Subject: [PATCH] Add logo to all pages, hide sidebar on login, create Admin Dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Logo Implementation: - Logo now appears on ALL pages automatically - Moved logo display to apply_custom_styles() function - 300px width, positioned at top with margin adjustments - Uses SBLM.jpg from src/notebookllama/ Login Page Improvements: - Sidebar completely hidden when not logged in - Cleaner login experience - No navigation shown to unauthenticated users - Title updated to "Sandbox-NotebookLM - Login" Admin Dashboard: - Replaced Observability Dashboard with Admin Dashboard - Shows platform usage statistics: * Total users, notebooks, documents, chat messages * Estimated costs (documents, chats, podcasts) * Recent activity (users, notebooks, documents) * Background task monitoring * User analytics (most active users) * System health (failed/pending tasks) Admin Access Control: - Only user ID 1 or users with "admin" in email can access - Admin link only shows in sidebar for admins - Protected with access check Cost Tracking: - Document processing: ~$0.60 each - Chat messages: ~$0.01 each - Podcasts: ~$0.50 each - Total cost estimate displayed Analytics Features: - Top 5 most active users by notebook count - Recent users/notebooks/documents lists - Background task status with color coding - System health indicators - Real-time statistics Files: - Created pages/5_Admin_Dashboard.py - Removed pages/5_Observability_Dashboard.py (backed up) - Updated styles.py with show_logo() function - Updated App.py navigation to show Admin conditionally 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/notebookllama/App.py | 28 ++- src/notebookllama/pages/5_Admin_Dashboard.py | 201 ++++++++++++++++++ ...y => OLD_5_Observability_Dashboard.py.bak} | 0 src/notebookllama/styles.py | 17 ++ 4 files changed, 231 insertions(+), 15 deletions(-) create mode 100644 src/notebookllama/pages/5_Admin_Dashboard.py rename src/notebookllama/pages/{5_Observability_Dashboard.py => OLD_5_Observability_Dashboard.py.bak} (100%) diff --git a/src/notebookllama/App.py b/src/notebookllama/App.py index 3562029..f14c56f 100644 --- a/src/notebookllama/App.py +++ b/src/notebookllama/App.py @@ -9,26 +9,21 @@ st.set_page_config( initial_sidebar_state="expanded" ) -# Apply custom styles +# Apply custom styles (includes logo) apply_custom_styles() -# Display logo at top -import base64 -from pathlib import Path - -logo_path = Path(__file__).parent / "SBLM.jpg" -if logo_path.exists(): - with open(logo_path, "rb") as f: - logo_data = base64.b64encode(f.read()).decode() - st.markdown( - f'
', - unsafe_allow_html=True - ) - # Check if user is logged in user = get_current_user() if not user: + # Hide sidebar when not logged in + st.markdown(""" + + """, unsafe_allow_html=True) show_login_page() else: # User is logged in - show sidebar with navigation @@ -45,7 +40,10 @@ else: st.page_link("App.py", label="🏠 Dashboard") st.page_link("pages/1_My_Notebooks.py", label="📚 My Notebooks") st.page_link("pages/4_Shared_Notebooks.py", label="🤝 Shared With Me") - st.page_link("pages/5_Observability_Dashboard.py", label="📊 Observability") + + # Admin link (only show if user is admin) + if user.id == 1 or "admin" in user.email.lower(): + st.page_link("pages/5_Admin_Dashboard.py", label="⚙️ Admin") # Main dashboard content st.title("Sandbox-NotebookLM Dashboard 🦙") diff --git a/src/notebookllama/pages/5_Admin_Dashboard.py b/src/notebookllama/pages/5_Admin_Dashboard.py new file mode 100644 index 0000000..5254bdf --- /dev/null +++ b/src/notebookllama/pages/5_Admin_Dashboard.py @@ -0,0 +1,201 @@ +import streamlit as st +from datetime import datetime, timedelta +from auth import require_auth, get_current_user +from styles import apply_custom_styles +from database import get_db, User, Notebook, Document, DocumentSummary, ChatSession, ChatMessage, BackgroundTask +from sqlalchemy import func, desc + +require_auth() +user = get_current_user() + +st.set_page_config(page_title="Sandbox-NotebookLM - Admin", page_icon="⚙️", layout="wide") +apply_custom_styles() + +st.title("⚙️ Admin Dashboard") +st.markdown("---") + +# Check if user is admin (for now, just check if they're the first user or have admin email) +is_admin = user.id == 1 or "admin" in user.email.lower() + +if not is_admin: + st.warning("⚠️ Admin access required") + st.info("This page is only accessible to administrators.") + st.stop() + +# Get database session +db = get_db() + +try: + # === USAGE STATISTICS === + st.subheader("📊 Platform Usage") + + col1, col2, col3, col4 = st.columns(4) + + with col1: + total_users = db.query(User).count() + st.metric("Total Users", total_users) + + with col2: + total_notebooks = db.query(Notebook).count() + st.metric("Total Notebooks", total_notebooks) + + with col3: + total_documents = db.query(Document).count() + st.metric("Total Documents", total_documents) + + with col4: + total_chats = db.query(ChatMessage).count() + st.metric("Total Chat Messages", total_chats) + + st.markdown("---") + + # === COST ESTIMATION === + st.subheader("💰 Estimated Costs") + + total_summaries = db.query(DocumentSummary).count() + podcasts_generated = db.query(Notebook).filter(Notebook.podcast_path.isnot(None)).count() + + col_c1, col_c2, col_c3, col_c4 = st.columns(4) + + with col_c1: + doc_cost = total_summaries * 0.60 + st.metric("Document Processing", f"${doc_cost:.2f}", help="~$0.60 per document") + + with col_c2: + chat_cost = total_chats * 0.01 + st.metric("Chat Messages", f"${chat_cost:.2f}", help="~$0.01 per message") + + with col_c3: + podcast_cost = podcasts_generated * 0.50 + st.metric("Podcasts Generated", f"${podcast_cost:.2f}", help="~$0.50 per podcast") + + with col_c4: + total_cost = doc_cost + chat_cost + podcast_cost + st.metric("Total Estimated", f"${total_cost:.2f}", delta=None) + + st.caption("💡 These are estimates based on average API usage. Actual costs may vary.") + + st.markdown("---") + + # === RECENT ACTIVITY === + st.subheader("🕐 Recent Activity") + + tab1, tab2, tab3 = st.tabs(["Users", "Notebooks", "Documents"]) + + with tab1: + st.markdown("**Recent Users:**") + recent_users = db.query(User).order_by(desc(User.created_at)).limit(10).all() + for u in recent_users: + col_u1, col_u2, col_u3 = st.columns([2, 2, 1]) + with col_u1: + st.write(f"👤 {u.username}") + with col_u2: + st.caption(u.email) + with col_u3: + st.caption(u.created_at.strftime('%Y-%m-%d')) + + with tab2: + st.markdown("**Recent Notebooks:**") + recent_notebooks = db.query(Notebook).order_by(desc(Notebook.created_at)).limit(10).all() + for nb in recent_notebooks: + col_n1, col_n2, col_n3 = st.columns([3, 2, 1]) + with col_n1: + st.write(f"📓 {nb.name}") + with col_n2: + st.caption(f"by {nb.owner.username}") + with col_n3: + st.caption(nb.created_at.strftime('%Y-%m-%d')) + + with tab3: + st.markdown("**Recent Documents:**") + recent_docs = db.query(Document).order_by(desc(Document.created_at)).limit(10).all() + for doc in recent_docs: + col_d1, col_d2, col_d3 = st.columns([3, 2, 1]) + with col_d1: + st.write(f"📄 {doc.original_filename}") + with col_d2: + st.caption(f"by {doc.owner.username}") + with col_d3: + st.caption(doc.created_at.strftime('%Y-%m-%d')) + + st.markdown("---") + + # === BACKGROUND TASKS === + st.subheader("⚙️ Background Tasks") + + tasks = db.query(BackgroundTask).order_by(desc(BackgroundTask.created_at)).limit(20).all() + + if tasks: + for task in tasks: + status_color = { + 'pending': '🟡', + 'in_progress': '🔵', + 'completed': '✅', + 'failed': '❌' + }.get(task.status.value, '⚪') + + col_t1, col_t2, col_t3, col_t4 = st.columns([2, 2, 1, 1]) + with col_t1: + st.write(f"{status_color} {task.task_type}") + with col_t2: + if task.notebook_id: + notebook = db.query(Notebook).filter(Notebook.id == task.notebook_id).first() + if notebook: + st.caption(f"Notebook: {notebook.name}") + with col_t3: + st.caption(task.status.value) + with col_t4: + st.caption(task.created_at.strftime('%H:%M')) + else: + st.info("No background tasks yet") + + st.markdown("---") + + # === USER ANALYTICS === + st.subheader("👥 User Analytics") + + # Top users by notebooks + top_users = db.query( + User.username, + func.count(Notebook.id).label('notebook_count') + ).join(Notebook).group_by(User.id, User.username).order_by(desc('notebook_count')).limit(5).all() + + if top_users: + st.markdown("**Most Active Users (by notebooks):**") + for username, count in top_users: + st.write(f"👤 {username}: {count} notebook(s)") + + st.markdown("---") + + # === SYSTEM HEALTH === + st.subheader("🏥 System Health") + + col_h1, col_h2, col_h3 = st.columns(3) + + with col_h1: + # Check failed tasks + failed_tasks = db.query(BackgroundTask).filter(BackgroundTask.status == 'failed').count() + if failed_tasks > 0: + st.error(f"⚠️ {failed_tasks} failed tasks") + else: + st.success("✅ No failed tasks") + + with col_h2: + # Check pending tasks + pending_tasks = db.query(BackgroundTask).filter( + BackgroundTask.status.in_(['pending', 'in_progress']) + ).count() + if pending_tasks > 0: + st.info(f"🔄 {pending_tasks} tasks in progress") + else: + st.success("✅ No pending tasks") + + with col_h3: + # Database size estimate + st.info(f"📊 {total_documents} docs + {podcasts_generated} podcasts") + +finally: + db.close() + +st.markdown("---") +st.caption("Admin Dashboard - Real-time platform statistics and monitoring") diff --git a/src/notebookllama/pages/5_Observability_Dashboard.py b/src/notebookllama/pages/OLD_5_Observability_Dashboard.py.bak similarity index 100% rename from src/notebookllama/pages/5_Observability_Dashboard.py rename to src/notebookllama/pages/OLD_5_Observability_Dashboard.py.bak diff --git a/src/notebookllama/styles.py b/src/notebookllama/styles.py index 2f112e2..fa3befe 100644 --- a/src/notebookllama/styles.py +++ b/src/notebookllama/styles.py @@ -170,7 +170,24 @@ def get_custom_css(): """ +def show_logo(): + """Display the logo at the top of the page""" + import streamlit as st + import base64 + from pathlib import Path + + logo_path = Path(__file__).parent / "SBLM.jpg" + if logo_path.exists(): + with open(logo_path, "rb") as f: + logo_data = base64.b64encode(f.read()).decode() + st.markdown( + f'
', + unsafe_allow_html=True + ) + + def apply_custom_styles(): """Apply custom CSS to the current page""" import streamlit as st st.markdown(get_custom_css(), unsafe_allow_html=True) + show_logo()