"""Streamlit entrypoint โ€” multi-page, multi-tenant admin dashboard.""" from __future__ import annotations import datetime as _dt import os import time from collections import Counter import streamlit as st from gh_deepagent.dashboard.api import APIError, parse_prometheus, sum_by, total from gh_deepagent.dashboard.auth_ui import ( is_standalone, render_user_badge, require_login, ) st.set_page_config( page_title="gh-deepagent", page_icon="๐Ÿค–", layout="wide", initial_sidebar_state="expanded", ) # ---------- sidebar (drawn before login so the user can change the API URL) ---------- with st.sidebar: st.title("๐Ÿค– gh-deepagent") st.caption("Admin dashboard") base = st.text_input( "API base URL", value=st.session_state.get("api_base") or os.getenv("DEEPAGENT_API_URL", "http://localhost:8080"), key="api_base_input", help="The gh-deepagent webhook server.", ) if base != st.session_state.get("api_base"): st.session_state["api_base"] = base st.session_state.pop("_backend_probed", None) st.session_state.setdefault("autorefresh", True) st.session_state["autorefresh"] = st.toggle( "Auto-refresh (10s)", value=st.session_state["autorefresh"] ) # ---------- login gate ---------- api, user = require_login() # Sidebar widgets that depend on being logged in. with st.sidebar: st.divider() if is_standalone(): st.warning("๐ŸŒ Standalone mode (no backend)") else: try: h = api.healthz() if h.get("status") == "ok": st.success( f"โœ… Healthy ยท queue {h.get('queue_depth', '?')} ยท " f"DLQ {h.get('dead_letter', '?')}" ) else: st.warning(f"โš ๏ธ Degraded ยท {h}") except APIError as e: st.error(f"โŒ Unreachable: {e}") render_user_badge() # ---------- helpers ---------- def _fmt_ts(ts): if not ts: return "โ€”" return _dt.datetime.fromtimestamp(float(ts)).strftime("%Y-%m-%d %H:%M:%S") # ---------- HOME page ---------- st.title("Overview") if user.get("is_admin"): _scope = "admin (sees all)" else: _n = len(user.get("installation_ids") or []) _scope = f"{_n} installation(s)" st.caption(f"Signed in as **{user['login']}** โ€” {_scope}.") if is_standalone(): st.info( "๐ŸŒ **Standalone mode** โ€” you're signed in directly against GitHub. " "Job / queue / cost data lives in a backend that isn't reachable from " "this Space. Configure `DEEPAGENT_API_URL` in the sidebar to point at " "your webhook, or deploy the all-in-one demo Space for a self-contained " "experience." ) st.subheader("Your GitHub App installations") iids = user.get("installation_ids") or [] if iids: for iid in iids[:50]: st.markdown(f"- installation `#{iid}`") else: st.caption( "You don't have access to any installation of a configured " "gh-deepagent GitHub App. Install one to see your scoped data here." ) elif user.get("is_admin"): try: metrics = parse_prometheus(api.metrics_raw()) except APIError as e: st.error(f"Failed to fetch /metrics: {e}") st.stop() def _val(name: str) -> float: return total(metrics.get(name, [])) k1, k2, k3, k4, k5 = st.columns(5) k1.metric("Queue depth", int(_val("deepagent_queue_depth"))) dlq = int(_val("deepagent_dlq_size")) k2.metric("DLQ", dlq, delta="โš ๏ธ" if dlq else None, delta_color="inverse" if dlq else "off") k3.metric("In-progress", int(_val("deepagent_jobs_in_progress"))) k4.metric("Jobs total", int(_val("deepagent_jobs_total"))) k5.metric("LLM spend (USD)", f"${_val('deepagent_llm_cost_usd_total'):.2f}") st.divider() c1, c2 = st.columns(2) with c1: st.subheader("Jobs by status") by_status = sum_by(metrics.get("deepagent_jobs_total", []), "status") if by_status: st.bar_chart(by_status, horizontal=True) else: st.info("No job activity yet.") with c2: st.subheader("Sub-agent invocations") by_sub = sum_by(metrics.get("deepagent_subagent_calls_total", []), "subagent") if by_sub: st.bar_chart(by_sub, horizontal=True) else: st.info("No sub-agent calls yet.") else: try: my_jobs = api.list_jobs(limit_per_install=50) except APIError as e: st.error(f"Failed to list jobs: {e}") st.stop() statuses = Counter(j["status"] for j in my_jobs) k1, k2, k3, k4 = st.columns(4) k1.metric("Recent jobs", len(my_jobs)) k2.metric("Running", statuses.get("running", 0) + statuses.get("pending", 0)) k3.metric("Failed", statuses.get("failed", 0) + statuses.get("dead", 0)) k4.metric("Succeeded", statuses.get("succeeded", 0)) st.divider() st.subheader("Your recent jobs") if not my_jobs: st.info("No jobs yet for your installations. Trigger one by labeling an " "issue with `deepagent` or commenting `/deepagent `.") else: import pandas as _pd df = _pd.DataFrame([{ "When": _fmt_ts(j.get("created_at")), "Repo": j["repo"], "Event": j["event"], "Status": j["status"], "ID": j["id"], } for j in my_jobs[:50]]) st.dataframe(df, use_container_width=True, hide_index=True) st.caption("Open a job by copying its ID into the **Jobs** page.") # Auto-refresh โ€” but not in standalone mode (nothing changes, just wastes cycles). if not is_standalone() and st.session_state.get("autorefresh"): time.sleep(10) st.rerun()