Spaces:
Sleeping
Sleeping
| # 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 |