dolev31 commited on
Commit
e84798f
Β·
1 Parent(s): f82bd7d

Add comprehensive admin key request dashboard

Browse files

Replace 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

Files changed (1) hide show
  1. app.py +169 -20
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 admin_view_key_requests(password: str) -> str:
1063
- """Show all key requests (admin only)."""
 
 
 
 
 
 
 
 
 
 
 
1064
  admin_pw = _get_admin_password()
1065
  if not admin_pw:
1066
- return "Admin password not configured. Set ADMIN_PASSWORD in Space secrets."
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
- lines = [f"Total key requests: {len(requests)}\n"]
1075
- for i, r in enumerate(requests, 1):
1076
- lines.append(
1077
- f"{i}. {r.get('email', '?')} | "
1078
- f"Team: {r.get('team', '?')} | "
1079
- f"Institution: {r.get('institution', '-')} | "
1080
- f"Time: {r.get('timestamp', '?')}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1081
  )
1082
- return "\n".join(lines)
 
 
 
 
 
 
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 Log", open=False):
1767
- gr.Markdown("View all signing key requests (email, team, institution, timestamp).")
 
 
 
1768
  admin_key_password = gr.Textbox(label="Admin Password", type="password")
1769
- admin_key_btn = gr.Button("View Key Requests")
1770
- admin_key_log = gr.Textbox(label="Key Requests", interactive=False, lines=20)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1771
 
1772
  admin_key_btn.click(
1773
- admin_view_key_requests,
1774
  inputs=[admin_key_password],
1775
- outputs=[admin_key_log],
 
 
 
 
 
 
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