Spaces:
Sleeping
Sleeping
File size: 29,066 Bytes
7d67a69 51c5a22 7d67a69 14640e3 7d67a69 604ea61 7d67a69 14640e3 fb04509 14640e3 fb04509 604ea61 7d67a69 14640e3 7d67a69 604ea61 7d67a69 604ea61 7d67a69 fb04509 7d67a69 14640e3 7d67a69 604ea61 7d67a69 51c5a22 7d67a69 14640e3 604ea61 14640e3 7d67a69 14640e3 7d67a69 51c5a22 7d67a69 604ea61 7d67a69 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 | # ui.py
# 此檔案負責 Gradio 用戶界面設計
# 包含登入界面、儀表板、管理員界面等
import gradio as gr
import pandas as pd
from config import ACTION_MAP, USER_DB, SYSTEM_WEIGHTS
from models import log_history, AI_READY
from auth import process_login, fast_login, logout, toggle_role, simulate_click, test_anomaly, unblock_user
from security import explain_abnormal_log
from plotting import get_dashboard_html, get_plot_data, timer_update
from utils import update_system_weights, get_current_time
from plotting import get_plot_data_for_api
# 自訂 CSS
custom_css = """
.mock-panel { background-color: var(--background-fill-secondary); padding: 15px; border-radius: 8px; border: 1px dashed var(--border-color-primary); }
.login-container { max-width: 500px; margin: 0 auto; padding: 20px; }
.ntut-title { color: #d9534f; font-size: 22px; font-weight: bold; text-align: center; margin-bottom: 20px;}
.weight-card { padding: 10px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-top: 10px; }
.headless-tabs > div:first-child { display: none !important; }
.headless-tabs { border: none !important; background: transparent !important; }
/* 統計儀表板精緻化樣式 */
.gradio-container .stat-dashboard { display: flex !important; gap: 20px !important; margin-bottom: 20px !important; justify-content: space-between !important; flex-wrap: nowrap !important; }
.gradio-container .stat-card {
flex: 1 !important;
background-color: #ffffff !important;
padding: 20px !important;
border-radius: 12px !important;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important;
display: flex !important;
align-items: center !important;
gap: 20px !important;
position: relative !important;
overflow: hidden !important;
transition: transform 0.2s ease-in-out !important;
min-width: 250px !important;
}
.gradio-container .stat-card:hover { transform: translateY(-2px) !important; }
/* 左側邊框色塊 */
.gradio-container .card-total { border-left: 6px solid #10b981 !important; }
.gradio-container .card-anomaly { border-left: 6px solid #ef4444 !important; }
.gradio-container .card-status { border-left: 6px solid #3b82f6 !important; border-top: 6px solid #10b981 !important; }
/* 圖示容器 */
.gradio-container .card-icon-container { width: 50px !important; height: 50px !important; display: flex !important; align-items: center !important; justify-content: center !important; }
.gradio-container .icon-bg { width: 45px !important; height: 45px !important; border-radius: 10px !important; display: flex !important; align-items: center !important; justify-content: center !important; }
/* 文字內容區域 */
.gradio-container .card-content { flex: 1 !important; display: flex !important; flex-direction: column !important; }
.gradio-container .card-label { color: #4b5563 !important; font-size: 14px !important; font-weight: 600 !important; margin-bottom: 4px !important; }
.gradio-container .card-value-group { display: flex !important; align-items: baseline !important; gap: 8px !important; }
.gradio-container .card-value { font-size: 28px !important; font-weight: 800 !important; line-height: 1.2 !important; color: #111827 !important; }
.gradio-container .card-unit { font-size: 14px !important; color: #6b7280 !important; font-weight: normal !important; }
.gradio-container .card-sub-text { color: #9ca3af !important; font-size: 12px !important; margin-top: 4px !important; }
/* 趨勢/狀態標籤 */
.gradio-container .card-trend { font-size: 12px !important; padding: 2px 8px !important; border-radius: 6px !important; font-weight: bold !important; margin-left: 8px !important; background-color: #fee2e2 !important; color: #ef4444 !important; }
.gradio-container .trend-up { background-color: #fee2e2 !important; color: #ef4444 !important; }
/* 系統狀態呼吸燈效果 */
.gradio-container .status-dot-container { width: 45px !important; height: 45px !important; display: flex !important; align-items: center !important; justify-content: center !important; }
.gradio-container .status-dot { width: 30px !important; height: 30px !important; border-radius: 50% !important; position: relative !important; }
.gradio-container .pulse-green { background-color: #10b981 !important; box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7) !important; animation: pulse-green 2s infinite !important; }
.gradio-container .pulse-yellow { background-color: #f59e0b !important; box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7) !important; animation: pulse-yellow 2s infinite !important; }
.gradio-container .pulse-red { background-color: #ef4444 !important; box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7) !important; animation: pulse-red 2s infinite !important; }
@keyframes pulse-green { 0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); } 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); } }
@keyframes pulse-yellow { 0% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(245, 158, 11, 0); } 100% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0); } }
@keyframes pulse-red { 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); } 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); } }
"""
def format_display_df(df):
"""格式化用於顯示的 Log DataFrame"""
if df is None or df.empty:
return pd.DataFrame(columns=["Timestamp", "IP Address", "Account", "Role", "Action", "Status"])
# 確保有這些欄位才進行處理
cols = df.columns.tolist()
# 1. 完整呈現 timestamp (日期與時間)
if "timestamp" in cols:
try:
# 轉換為 datetime 後統一轉回字串格式,確保日期與時間均顯示
df['timestamp'] = pd.to_datetime(df['timestamp']).dt.strftime('%Y-%m-%d %H:%M:%S')
except Exception:
# 若轉換失敗則確保是字串
df['timestamp'] = df['timestamp'].astype(str)
# 2. 將動作代碼對照為簡短英文名稱
if "action" in cols:
action_map_en = {
'click_eval_system': 'Evaluation',
'click_pre_select_system': 'Pre-Selection',
'click_add_drop_system': 'Add/Drop',
'click_istudy': 'iStudy',
'click_card_loss': 'Card Loss',
'click_course_system': 'Course System',
'click_score_query': 'Score Query',
'click_leave_system': 'Leave System',
'click_dorm_system': 'Dorm System',
'click_scholarship': 'Scholarship',
'click_webmail': 'WebMail',
'click_vdesk': 'Software Cloud',
'malicious_sql_injection': 'SQL Injection',
'login_attempt': 'Login',
'logout': 'Logout',
'Unknown': 'Unknown'
}
df['action'] = df['action'].apply(lambda x: action_map_en.get(str(x), str(x)))
# 3. 將角色對照為英文
if "role" in cols:
df['role'] = df['role'].apply(lambda x: "Student" if str(x) == "學生" else ("Admin" if str(x) == "管理" else str(x)))
# 4. 將判定狀態對照為簡短英文
if "status" in cols:
def map_status_to_en(val):
val_str = str(val)
if "異常" in val_str or "錯誤" in val_str or "受限" in val_str or "fail" in val_str.lower() or "block" in val_str.lower():
return "Abnormal"
return "Normal"
df['status'] = df['status'].apply(map_status_to_en)
# 5. 移除 cookie_id,並重新排列欄位與改名
rename_dict = {
"timestamp": "Timestamp",
"ip_address": "IP Address",
"account": "Account",
"role": "Role",
"action": "Action",
"status": "Status"
}
# 篩選並保留除了 cookie_id 之外的欄位
keep_cols = [c for c in ["timestamp", "ip_address", "account", "role", "action", "status"] if c in cols]
df_filtered = df[keep_cols].copy()
# 重新命名欄位
df_filtered = df_filtered.rename(columns=rename_dict)
# 確保所有需要的欄位都存在 (若不存在則填空欄位)
for target_col in ["Timestamp", "IP Address", "Account", "Role", "Action", "Status"]:
if target_col not in df_filtered.columns:
df_filtered[target_col] = ""
# 重新排序
df_filtered = df_filtered[["Timestamp", "IP Address", "Account", "Role", "Action", "Status"]]
return df_filtered
def toggle_time_input(mode):
"""切換時間輸入模式"""
return gr.update(visible=(mode == "自訂時間 (模擬過去/未來)"))
def full_ui_update(selected_key):
"""更新所有 UI 元件:儀表板、圖表、所有 Log、異常 Log"""
# 1. 獲取儀表板 HTML
dash_html = get_dashboard_html()
# 2. 獲取圖表 (直接使用 matplotlib Figure,交給 gr.Plot 渲染)
plot_fig = get_plot_data(selected_key)
# 3. 獲取 Log 資料
df_all = pd.DataFrame(log_history)
abnormal_logs = [log for log in log_history if any(word in str(log.get("status", "")) for word in ["異常", "錯誤", "受限"])]
df_abn = pd.DataFrame(abnormal_logs) if abnormal_logs else pd.DataFrame(columns=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"])
return dash_html, plot_fig, format_display_df(df_all), format_display_df(df_abn)
def create_demo():
"""創建 Gradio 應用"""
with gr.Blocks(title="模擬校園入口與資安防護系統") as demo:
gr.HTML(f"<style>{custom_css}</style>")
demo.css = custom_css
store_df_all = gr.State(value=pd.DataFrame(log_history))
store_df_abn = gr.State(value=pd.DataFrame(columns=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"]))
dashboard_timer = gr.Timer(value=60)
with gr.Row():
with gr.Column(scale=1, elem_classes="mock-panel"):
gr.Markdown("### ⚙️ 環境變數模擬器")
mock_ip = gr.Dropdown(
choices=["140.124.71.55 (校內預設)", "140.124.18.22 (宿舍)", "61.228.45.112 (家裡)", "210.61.47.88 (公共場所)", "45.33.22.11 (國外異常IP)"],
value="140.124.71.55 (校內預設)",
allow_custom_value=True,
label="模擬來源 IP"
)
mock_cookie = gr.Textbox(label="模擬 Cookie Session ID", value="sess_baseline")
gr.Markdown("---")
time_mode = gr.Radio(
label="時間設定模式",
choices=["真實時間 (目前時間)", "自訂時間 (模擬過去/未來)"],
value="真實時間 (目前時間)"
)
custom_time_input = gr.Textbox(
label="輸入自訂時間",
placeholder="格式: YYYY-MM-DD HH:MM:SS",
value="2026-03-17 03:00:00",
visible=False
)
gr.Markdown("---")
gr.Markdown("### 📊 即時 Log 與 AI 判定區")
log_display = gr.Dataframe(
value=format_display_df(pd.DataFrame(log_history)),
headers=["Timestamp", "IP Address", "Account", "Role", "Action", "Status"],
datatype=["str", "str", "str", "str", "str", "str"],
interactive=False,
wrap=True
)
with gr.Column(scale=2):
with gr.Tabs(selected="login_route", elem_classes="headless-tabs") as main_router:
with gr.Tab("Login", id="login_route"):
with gr.Column(elem_classes="login-container"):
gr.Markdown("<div class='ntut-title'>校園入口網站 Taipei Tech Portal</div>")
login_msg = gr.Markdown(value="", visible=True)
with gr.Row():
acc_input = gr.Textbox(label="使用者帳號 (Account)", placeholder="student or admin")
with gr.Row():
pwd_input = gr.Textbox(label="使用者密碼 (Password)", type="password", placeholder="password : 1234")
with gr.Row():
gr.HTML("<div style='background:var(--background-fill-secondary); color:var(--body-text-color); padding:10px; font-weight:bold; letter-spacing: 3px; border:1px solid var(--border-color-primary); text-align:center;'>F E I K</div>")
with gr.Row():
captcha_input = gr.Textbox(label="請輸入驗證碼 (Keyin Code)")
login_btn = gr.Button("登入 Login", variant="primary")
gr.Markdown("---")
gr.Markdown("<div style='text-align:center; color:gray; font-size: 0.9em;'>🛠️ 開發與測試專用捷徑</div>")
with gr.Row():
btn_fast_student = gr.Button("🚀 快速登入 (學生)", variant="secondary")
btn_fast_admin = gr.Button("🚀 快速登入 (管理員)", variant="secondary")
with gr.Tab("Dashboard", id="dashboard_route"):
with gr.Column():
with gr.Row():
gr.Markdown("### 資訊系統")
welcome_text = gr.Markdown(value="", elem_classes="text-right")
logout_btn = gr.Button("登出", size="sm")
with gr.Row():
btn_toggle_role = gr.Button("🔄 Student ↔ Admin", size="sm", variant="secondary")
gr.Markdown("---")
with gr.Tabs(selected="student_tab", elem_classes="headless-tabs") as role_tabs:
with gr.Tab("👨🎓 學生專區", id="student_tab"):
with gr.Accordion("▼ 1. 教務系統", open=True):
with gr.Row():
btn_eval = gr.Button("▶ 期末網路教學評量", size="sm")
btn_pre_select = gr.Button("▶ 期末網路預選系統", size="sm")
btn_add_drop = gr.Button("▶ 開學後加退選系統", size="sm")
with gr.Row():
btn_istudy = gr.Button("▶ 北科i學園PLUS", size="sm")
btn_card = gr.Button("▶ 學生證掛失及補發系統", size="sm")
btn_course = gr.Button("▶ 課程系統", size="sm")
with gr.Row():
btn_score = gr.Button("▶ 學業成績查詢系統", size="sm")
with gr.Accordion("▼ 2. 學務系統", open=True):
with gr.Row():
btn_leave = gr.Button("▶ 學生請假系統", size="sm")
btn_dorm = gr.Button("▶ 學生宿舍登錄(抽籤)系統", size="sm")
btn_scholarship = gr.Button("▶ 獎助學金申請系統", size="sm")
with gr.Accordion("▼ 3. 資訊服務", open=True):
with gr.Row():
btn_webmail = gr.Button("▶ 網路郵局 WebMail", size="sm")
btn_vdesk = gr.Button("▶ 北科軟體雲", size="sm")
with gr.Tab("🛡️ 管理員專區", id="admin_tab"):
stat_dashboard_display = gr.HTML(value=get_dashboard_html())
with gr.Accordion("▼ 系統操作時間分佈圖 (即時監控)", open=True, elem_classes="weight-card"):
action_dropdown = gr.Dropdown(choices=list(ACTION_MAP.keys()), label="選擇要檢視的系統資料分佈", value=list(ACTION_MAP.keys())[0])
action_plot = gr.Plot(label="圖表顯示區")
with gr.Accordion("▼ 防護強度微調 (AI 敏感度)", open=True, elem_classes="weight-card"):
gr.Markdown("<span style='color:gray; font-size:0.9em;'>動態防護已全面啟動:包含 HDBSCAN 空間異常偵測、IP 聲譽聯防,以及動態視窗頻率攔截。</span>")
with gr.Row():
overall_weight = gr.Number(label="總體防護強度 (%)", value=75, minimum=0, maximum=100)
time_weight = gr.Number(label="時間特徵權重 (%)", value=80, minimum=0, maximum=100)
ip_weight = gr.Number(label="IP 聲譽權重 (%)", value=65, minimum=0, maximum=100)
with gr.Row():
device_weight = gr.Number(label="裝置指紋/行為權重 (%)", value=45, minimum=0, maximum=100)
geo_weight = gr.Number(label="🌍 地理偏移敏感度 (%)", value=50, minimum=0, maximum=100)
freq_weight = gr.Number(label="⚡ 行為頻率敏感度 (%)", value=60, minimum=0, maximum=100)
weights_status = gr.Textbox(value="", visible=False)
with gr.Accordion("▼ 異常 Log 分析 (Groq AI Agent)", open=True, elem_classes="weight-card"):
initial_abnormal = [log for log in log_history if any(word in str(log.get("status", "")) for word in ["異常", "錯誤", "受限"])]
abnormal_log_display = gr.Dataframe(
value=format_display_df(pd.DataFrame(initial_abnormal)),
headers=["Timestamp", "IP Address", "Account", "Role", "Action", "Status"],
datatype=["str", "str", "str", "str", "str", "str"],
interactive=False,
wrap=True
)
with gr.Row():
abnormal_log_input = gr.Textbox(label="要分析的異常 Log", placeholder="系統若偵測到新異常將會自動填入...", scale=4)
btn_explain_log = gr.Button("🧠 Groq AI 分析", variant="primary", scale=1)
ai_explanation_output = gr.Textbox(label="AI 分析結果與說明", lines=4, interactive=False)
with gr.Accordion("▼ 使用者狀態管理", open=True, elem_classes="weight-card"):
from config import FIXED_TEST_EMAIL
gr.Markdown(f"<span style='color:gray; font-size:0.9em;'>手動解除對測試帳號 ({FIXED_TEST_EMAIL}) 的封鎖狀態。</span>")
btn_unblock = gr.Button("🔓 解除封鎖 (Unblock User)", variant="primary")
unblock_status = gr.Markdown(value="")
with gr.Accordion("▼ HDBSCAN 異常辨識自動化測試區", open=True, elem_classes="weight-card"):
gr.Markdown("<span style='color:gray; font-size:0.9em;'>點擊按鈕將直接以指定時間點執行該系統的操作,結果將顯示於左側 Log 區。</span>")
gr.Markdown("#### 1. 選課相關系統 (具備強烈季節性)")
with gr.Row():
t_pre_sel_norm = gr.Button("✅ 正常: 期末網路預選系統 (06-13 11:00)", size="sm")
t_pre_sel_anom = gr.Button("⚠️ 異常: 期末網路預選系統 (10-15 13:00)", size="sm")
with gr.Row():
t_course_norm = gr.Button("✅ 正常: 課程系統 (02-25 07:50)", size="sm")
t_course_anom = gr.Button("⚠️ 異常: 課程系統 (07-15 05:00)", size="sm")
with gr.Row():
t_add_drop_norm = gr.Button("✅ 正常: 開學後加退選系統 (02-18 14:00)", size="sm")
t_add_drop_anom = gr.Button("⚠️ 異常: 開學後加退選系統 (05-15 12:00)", size="sm")
gr.Markdown("#### 2. 期末與成績相關 (具備週期性)")
with gr.Row():
t_eval_norm = gr.Button("✅ 正常: 期末網路教學評量 (12-17 13:20)", size="sm")
t_eval_anom = gr.Button("⚠️ 異常: 期末網路教學評量 (03-15 03:00)", size="sm")
with gr.Row():
t_score_norm = gr.Button("✅ 正常: 學業成績查詢系統 (11-17 18:50)", size="sm")
t_score_anom = gr.Button("⚠️ 異常: 學業成績查詢系統 (02-15 03:00)", size="sm")
gr.Markdown("#### 3. 校務與行政服務 (生活與常規)")
with gr.Row():
t_dorm_norm = gr.Button("✅ 正常: 學生宿舍登錄(抽籤)系統 (07-21 23:10)", size="sm")
t_dorm_anom = gr.Button("⚠️ 異常: 學生宿舍登錄(抽籤)系統 (11-15 16:00)", size="sm")
with gr.Row():
t_schol_norm = gr.Button("✅ 正常: 獎助學金申請系統 (03-12 14:00)", size="sm")
t_schol_anom = gr.Button("⚠️ 異常: 獎助學金申請系統 (08-15 02:00)", size="sm")
with gr.Row():
t_card_norm = gr.Button("✅ 正常: 學生證掛失 (10-23 13:40)", size="sm")
t_card_anom = gr.Button("⚠️ 異常: 學生證掛失 (08-10 03:00)", size="sm")
gr.Markdown("#### 4. 學習與日常輔助系統 (具備作息規律)")
with gr.Row():
t_istudy_norm = gr.Button("✅ 正常: 北科i學園PLUS (10-18 16:00)", size="sm")
t_istudy_anom = gr.Button("⚠️ 異常: 北科i學園PLUS (08-20 02:00)", size="sm")
with gr.Row():
t_leave_norm = gr.Button("✅ 正常: 學生請假系統 (03-29 08:45)", size="sm")
t_leave_anom = gr.Button("⚠️ 異常: 學生請假系統 (08-15 13:00)", size="sm")
with gr.Row():
t_vdesk_norm = gr.Button("✅ 正常: 軟體雲/WebMail (03-22 18:00)", size="sm")
t_vdesk_anom = gr.Button("⚠️ 異常: 軟體雲/WebMail (08-10 00:00)", size="sm")
# 事件綁定
def sync_log_displays(df_all, df_abn):
return format_display_df(df_all), format_display_df(df_abn)
store_df_all.change(fn=sync_log_displays, inputs=[store_df_all, store_df_abn], outputs=[log_display, abnormal_log_display])
demo.load(
fn=full_ui_update,
inputs=[action_dropdown],
outputs=[stat_dashboard_display, action_plot, log_display, abnormal_log_display]
)
dashboard_timer.tick(
fn=full_ui_update,
inputs=[action_dropdown],
outputs=[stat_dashboard_display, action_plot, log_display, abnormal_log_display]
)
action_dropdown.change(fn=full_ui_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot, log_display, abnormal_log_display])
time_mode.change(fn=toggle_time_input, inputs=time_mode, outputs=custom_time_input)
btn_explain_log.click(fn=explain_abnormal_log, inputs=[abnormal_log_input], outputs=[ai_explanation_output])
def handle_unblock():
from config import FIXED_TEST_EMAIL
success = unblock_user(FIXED_TEST_EMAIL)
if success:
gr.Info(f"已成功解除 {FIXED_TEST_EMAIL} 的封鎖狀態。")
return "✅ 狀態已重設為 Active"
else:
return "❌ 找不到該使用者或已是 Active 狀態"
btn_unblock.click(fn=handle_unblock, inputs=[], outputs=[unblock_status])
update_outputs = [main_router, role_tabs, login_msg, welcome_text, store_df_all, store_df_abn, abnormal_log_input]
login_btn.click(fn=process_login, inputs=[mock_ip, mock_cookie, acc_input, pwd_input, captcha_input, time_mode, custom_time_input], outputs=update_outputs)
logout_btn.click(fn=logout, inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input], outputs=[main_router, acc_input, pwd_input, captcha_input, login_msg, store_df_all, store_df_abn, abnormal_log_input])
fast_inputs = [mock_ip, mock_cookie, time_mode, custom_time_input]
fast_outputs = update_outputs + [acc_input]
btn_fast_student.click(
fn=lambda ip, c, tm, ct: fast_login("student", ip, c, tm, ct),
inputs=fast_inputs,
outputs=fast_outputs,
api_name=None
)
btn_fast_admin.click(
fn=lambda ip, c, tm, ct: fast_login("admin", ip, c, tm, ct),
inputs=fast_inputs,
outputs=fast_outputs,
api_name=None
)
btn_toggle_role.click(fn=toggle_role, inputs=[acc_input, mock_ip, mock_cookie, time_mode, custom_time_input], outputs=[role_tabs, welcome_text, store_df_all, store_df_abn, abnormal_log_input, acc_input])
action_mapping = [
(btn_eval, "click_eval_system"), (btn_pre_select, "click_pre_select_system"), (btn_add_drop, "click_add_drop_system"),
(btn_istudy, "click_istudy"), (btn_card, "click_card_loss"), (btn_course, "click_course_system"),
(btn_score, "click_score_query"), (btn_leave, "click_leave_system"), (btn_dorm, "click_dorm_system"),
(btn_scholarship, "click_scholarship"), (btn_webmail, "click_webmail"), (btn_vdesk, "click_vdesk")
]
for btn, action_name in action_mapping:
btn.click(
fn=lambda ip, c, acc, tm, ct, n=action_name: simulate_click(ip, c, acc, n, tm, ct),
inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input],
outputs=[store_df_all, store_df_abn, abnormal_log_input],
api_name=None
)
test_mapping = [
(t_pre_sel_norm, "click_pre_select_system", "2026-06-13 11:00:00"), (t_pre_sel_anom, "click_pre_select_system", "2026-10-15 13:00:00"),
(t_course_norm, "click_course_system", "2026-02-25 07:50:00"), (t_course_anom, "click_course_system", "2026-07-15 05:00:00"),
(t_add_drop_norm, "click_add_drop_system", "2026-02-18 14:00:00"), (t_add_drop_anom, "click_add_drop_system", "2026-05-15 12:00:00"),
(t_eval_norm, "click_eval_system", "2026-12-17 13:20:00"), (t_eval_anom, "click_eval_system", "2026-03-15 03:00:00"),
(t_score_norm, "click_score_query", "2026-11-17 18:50:00"), (t_score_anom, "click_score_query", "2026-02-15 03:00:00")
]
for btn, action_name, test_time_str in test_mapping:
btn.click(
fn=lambda ip, c, acc, n=action_name, t_str=test_time_str: test_anomaly(ip, c, acc, n, t_str),
inputs=[mock_ip, mock_cookie, acc_input],
outputs=[store_df_all, store_df_abn, abnormal_log_input],
api_name=None
)
return demo |