Spaces:
Runtime error
Runtime error
Add comprehensive admin key request dashboard
Browse filesReplace plain-text key request log with full dashboard:
- Summary statistics (totals, activity, top teams/institutions)
- Cumulative timeline chart + institution bar chart
- Sortable data table + CSV download
app.py
CHANGED
|
@@ -14,7 +14,9 @@ import json
|
|
| 14 |
import logging
|
| 15 |
import os
|
| 16 |
import re
|
|
|
|
| 17 |
import traceback
|
|
|
|
| 18 |
from datetime import datetime, timezone
|
| 19 |
from enum import Enum
|
| 20 |
from pathlib import Path
|
|
@@ -1059,27 +1061,150 @@ def admin_remove_submission(agent_id: str, password: str):
|
|
| 1059 |
return f"Removed {removed} submission(s) with agent_id '{agent_id}'."
|
| 1060 |
|
| 1061 |
|
| 1062 |
-
def
|
| 1063 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1064 |
admin_pw = _get_admin_password()
|
| 1065 |
if not admin_pw:
|
| 1066 |
-
return "Admin password not configured.
|
| 1067 |
if password != admin_pw:
|
| 1068 |
-
return "Invalid admin password."
|
| 1069 |
|
| 1070 |
requests = _load_key_requests()
|
| 1071 |
if not requests:
|
| 1072 |
-
return "No key requests yet."
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1081 |
)
|
| 1082 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1083 |
|
| 1084 |
|
| 1085 |
def admin_login(password: str):
|
|
@@ -1763,16 +1888,40 @@ contact details.
|
|
| 1763 |
api_name=False,
|
| 1764 |
)
|
| 1765 |
|
| 1766 |
-
with gr.Accordion("Key Request
|
| 1767 |
-
gr.Markdown(
|
|
|
|
|
|
|
|
|
|
| 1768 |
admin_key_password = gr.Textbox(label="Admin Password", type="password")
|
| 1769 |
-
admin_key_btn = gr.Button("
|
| 1770 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1771 |
|
| 1772 |
admin_key_btn.click(
|
| 1773 |
-
|
| 1774 |
inputs=[admin_key_password],
|
| 1775 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1776 |
api_name=False,
|
| 1777 |
)
|
| 1778 |
|
|
|
|
| 14 |
import logging
|
| 15 |
import os
|
| 16 |
import re
|
| 17 |
+
import tempfile
|
| 18 |
import traceback
|
| 19 |
+
from collections import Counter
|
| 20 |
from datetime import datetime, timezone
|
| 21 |
from enum import Enum
|
| 22 |
from pathlib import Path
|
|
|
|
| 1061 |
return f"Removed {removed} submission(s) with agent_id '{agent_id}'."
|
| 1062 |
|
| 1063 |
|
| 1064 |
+
def admin_build_key_dashboard(password: str):
|
| 1065 |
+
"""Build comprehensive key request dashboard (admin only).
|
| 1066 |
+
|
| 1067 |
+
Returns (stats_markdown, dataframe, timeline_plot, institution_plot, csv_file).
|
| 1068 |
+
"""
|
| 1069 |
+
empty = (
|
| 1070 |
+
"*Enter password and click Load Dashboard*",
|
| 1071 |
+
pd.DataFrame(),
|
| 1072 |
+
_empty_figure("No data", 350),
|
| 1073 |
+
_empty_figure("No data", 300),
|
| 1074 |
+
None,
|
| 1075 |
+
)
|
| 1076 |
+
|
| 1077 |
admin_pw = _get_admin_password()
|
| 1078 |
if not admin_pw:
|
| 1079 |
+
return ("Admin password not configured.", *empty[1:])
|
| 1080 |
if password != admin_pw:
|
| 1081 |
+
return ("Invalid admin password.", *empty[1:])
|
| 1082 |
|
| 1083 |
requests = _load_key_requests()
|
| 1084 |
if not requests:
|
| 1085 |
+
return ("No key requests yet.", *empty[1:])
|
| 1086 |
+
|
| 1087 |
+
# ---- Build DataFrame ----
|
| 1088 |
+
df = pd.DataFrame(requests)
|
| 1089 |
+
df["timestamp"] = pd.to_datetime(df["timestamp"], errors="coerce", utc=True)
|
| 1090 |
+
df = df.sort_values("timestamp", ascending=False).reset_index(drop=True)
|
| 1091 |
+
|
| 1092 |
+
# ---- Summary Statistics ----
|
| 1093 |
+
total = len(df)
|
| 1094 |
+
unique_emails = df["email"].nunique()
|
| 1095 |
+
unique_teams = df["team"].nunique()
|
| 1096 |
+
inst_series = df["institution"].replace("", pd.NA).dropna()
|
| 1097 |
+
unique_institutions = inst_series.nunique()
|
| 1098 |
+
|
| 1099 |
+
now_utc = datetime.now(timezone.utc)
|
| 1100 |
+
last_7d = int(df[df["timestamp"] >= (now_utc - pd.Timedelta(days=7))].shape[0])
|
| 1101 |
+
last_30d = int(df[df["timestamp"] >= (now_utc - pd.Timedelta(days=30))].shape[0])
|
| 1102 |
+
|
| 1103 |
+
ts_min = df["timestamp"].min()
|
| 1104 |
+
ts_max = df["timestamp"].max()
|
| 1105 |
+
earliest = ts_min.strftime("%Y-%m-%d") if pd.notna(ts_min) else "N/A"
|
| 1106 |
+
latest = ts_max.strftime("%Y-%m-%d") if pd.notna(ts_max) else "N/A"
|
| 1107 |
+
|
| 1108 |
+
email_counts = Counter(df["email"])
|
| 1109 |
+
repeat_users = {e: c for e, c in email_counts.items() if c > 1}
|
| 1110 |
+
repeat_str = f"{len(repeat_users)} user(s)" if repeat_users else "None"
|
| 1111 |
+
|
| 1112 |
+
team_counts = Counter(df["team"])
|
| 1113 |
+
top_teams = team_counts.most_common(5)
|
| 1114 |
+
top_teams_str = ", ".join(f"{t} ({c})" for t, c in top_teams) if top_teams else "N/A"
|
| 1115 |
+
|
| 1116 |
+
inst_counts = Counter(inst_series)
|
| 1117 |
+
top_insts = inst_counts.most_common(5)
|
| 1118 |
+
top_insts_str = ", ".join(f"{t} ({c})" for t, c in top_insts) if top_insts else "N/A"
|
| 1119 |
+
|
| 1120 |
+
stats_md = (
|
| 1121 |
+
"### Key Request Statistics\n"
|
| 1122 |
+
"| Metric | Value |\n"
|
| 1123 |
+
"|:--|:--|\n"
|
| 1124 |
+
f"| **Total Requests** | {total} |\n"
|
| 1125 |
+
f"| **Unique Emails** | {unique_emails} |\n"
|
| 1126 |
+
f"| **Unique Teams** | {unique_teams} |\n"
|
| 1127 |
+
f"| **Unique Institutions** | {unique_institutions} |\n"
|
| 1128 |
+
f"| **Last 7 Days** | {last_7d} |\n"
|
| 1129 |
+
f"| **Last 30 Days** | {last_30d} |\n"
|
| 1130 |
+
f"| **Date Range** | {earliest} to {latest} |\n"
|
| 1131 |
+
f"| **Repeat Requesters** | {repeat_str} |\n"
|
| 1132 |
+
f"| **Top Teams** | {top_teams_str} |\n"
|
| 1133 |
+
f"| **Top Institutions** | {top_insts_str} |\n"
|
| 1134 |
+
)
|
| 1135 |
+
|
| 1136 |
+
# ---- Timeline Chart (Cumulative) ----
|
| 1137 |
+
timeline_fig = _empty_figure("No valid timestamps", 350)
|
| 1138 |
+
if pd.notna(df["timestamp"]).any():
|
| 1139 |
+
daily = (
|
| 1140 |
+
df.set_index("timestamp")
|
| 1141 |
+
.resample("D")
|
| 1142 |
+
.size()
|
| 1143 |
+
.cumsum()
|
| 1144 |
+
.reset_index(name="cumulative")
|
| 1145 |
+
)
|
| 1146 |
+
daily.columns = ["date", "cumulative"]
|
| 1147 |
+
timeline_fig = go.Figure()
|
| 1148 |
+
timeline_fig.add_trace(go.Scatter(
|
| 1149 |
+
x=daily["date"],
|
| 1150 |
+
y=daily["cumulative"],
|
| 1151 |
+
mode="lines+markers",
|
| 1152 |
+
line=dict(color=PLOTLY_COLORWAY[0], width=2),
|
| 1153 |
+
marker=dict(size=4),
|
| 1154 |
+
name="Cumulative Requests",
|
| 1155 |
+
fill="tozeroy",
|
| 1156 |
+
fillcolor="rgba(59, 130, 246, 0.1)",
|
| 1157 |
+
))
|
| 1158 |
+
timeline_fig.update_layout(**_plotly_layout(
|
| 1159 |
+
title="Key Requests Over Time (Cumulative)",
|
| 1160 |
+
xaxis_title="Date",
|
| 1161 |
+
yaxis_title="Total Requests",
|
| 1162 |
+
height=350,
|
| 1163 |
+
xaxis=dict(gridcolor=PLOTLY_GRID_COLOR),
|
| 1164 |
+
yaxis=dict(gridcolor=PLOTLY_GRID_COLOR, rangemode="tozero"),
|
| 1165 |
+
))
|
| 1166 |
+
|
| 1167 |
+
# ---- Institution Bar Chart ----
|
| 1168 |
+
if inst_counts:
|
| 1169 |
+
top_n = 10
|
| 1170 |
+
sorted_insts = inst_counts.most_common(top_n)
|
| 1171 |
+
inst_names = [x[0] for x in reversed(sorted_insts)]
|
| 1172 |
+
inst_vals = [x[1] for x in reversed(sorted_insts)]
|
| 1173 |
+
inst_fig = go.Figure(go.Bar(
|
| 1174 |
+
x=inst_vals,
|
| 1175 |
+
y=inst_names,
|
| 1176 |
+
orientation="h",
|
| 1177 |
+
marker_color=PLOTLY_COLORWAY[1],
|
| 1178 |
+
))
|
| 1179 |
+
inst_fig.update_layout(**_plotly_layout(
|
| 1180 |
+
title=f"Top {min(top_n, len(sorted_insts))} Institutions",
|
| 1181 |
+
xaxis_title="Requests",
|
| 1182 |
+
height=max(250, 40 * len(sorted_insts) + 100),
|
| 1183 |
+
yaxis=dict(tickfont=dict(size=11)),
|
| 1184 |
+
xaxis=dict(gridcolor=PLOTLY_GRID_COLOR, dtick=1),
|
| 1185 |
+
))
|
| 1186 |
+
else:
|
| 1187 |
+
inst_fig = _empty_figure("No institutions recorded", 300)
|
| 1188 |
+
|
| 1189 |
+
# ---- Display DataFrame ----
|
| 1190 |
+
display_df = df.copy()
|
| 1191 |
+
display_df["timestamp"] = display_df["timestamp"].dt.strftime("%Y-%m-%d %H:%M UTC")
|
| 1192 |
+
display_df.insert(0, "#", range(1, len(display_df) + 1))
|
| 1193 |
+
display_df.columns = ["#", "Email", "Team", "Institution", "Timestamp"]
|
| 1194 |
+
|
| 1195 |
+
# ---- CSV export ----
|
| 1196 |
+
csv_path = None
|
| 1197 |
+
try:
|
| 1198 |
+
tmp = tempfile.NamedTemporaryFile(
|
| 1199 |
+
mode="w", suffix=".csv", prefix="key_requests_", delete=False,
|
| 1200 |
)
|
| 1201 |
+
display_df.to_csv(tmp.name, index=False)
|
| 1202 |
+
tmp.close()
|
| 1203 |
+
csv_path = tmp.name
|
| 1204 |
+
except Exception:
|
| 1205 |
+
pass
|
| 1206 |
+
|
| 1207 |
+
return stats_md, display_df, timeline_fig, inst_fig, csv_path
|
| 1208 |
|
| 1209 |
|
| 1210 |
def admin_login(password: str):
|
|
|
|
| 1888 |
api_name=False,
|
| 1889 |
)
|
| 1890 |
|
| 1891 |
+
with gr.Accordion("Key Request Dashboard", open=False):
|
| 1892 |
+
gr.Markdown(
|
| 1893 |
+
"Comprehensive view of all signing key requests. "
|
| 1894 |
+
"Enter admin password and click **Load Dashboard**."
|
| 1895 |
+
)
|
| 1896 |
admin_key_password = gr.Textbox(label="Admin Password", type="password")
|
| 1897 |
+
admin_key_btn = gr.Button("Load Dashboard", variant="secondary")
|
| 1898 |
+
|
| 1899 |
+
admin_key_stats = gr.Markdown(
|
| 1900 |
+
value="*Enter password and click Load Dashboard*"
|
| 1901 |
+
)
|
| 1902 |
+
with gr.Row():
|
| 1903 |
+
admin_timeline_plot = gr.Plot(label="Requests Over Time")
|
| 1904 |
+
admin_inst_plot = gr.Plot(label="Requests by Institution")
|
| 1905 |
+
admin_key_table = gr.Dataframe(
|
| 1906 |
+
label="All Key Requests (newest first)",
|
| 1907 |
+
interactive=False,
|
| 1908 |
+
wrap=True,
|
| 1909 |
+
)
|
| 1910 |
+
admin_csv_download = gr.File(
|
| 1911 |
+
label="Download CSV",
|
| 1912 |
+
interactive=False,
|
| 1913 |
+
)
|
| 1914 |
|
| 1915 |
admin_key_btn.click(
|
| 1916 |
+
admin_build_key_dashboard,
|
| 1917 |
inputs=[admin_key_password],
|
| 1918 |
+
outputs=[
|
| 1919 |
+
admin_key_stats,
|
| 1920 |
+
admin_key_table,
|
| 1921 |
+
admin_timeline_plot,
|
| 1922 |
+
admin_inst_plot,
|
| 1923 |
+
admin_csv_download,
|
| 1924 |
+
],
|
| 1925 |
api_name=False,
|
| 1926 |
)
|
| 1927 |
|