File size: 26,193 Bytes
7d67a69
 
 
 
 
 
 
 
51c5a22
7d67a69
 
 
 
 
 
 
 
 
 
 
 
 
14640e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7d67a69
 
 
 
 
 
14640e3
 
 
 
fb04509
 
 
 
14640e3
 
 
 
fb04509
 
7d67a69
 
 
 
14640e3
 
7d67a69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fb04509
 
7d67a69
 
 
 
 
 
 
 
 
 
 
 
 
 
14640e3
7d67a69
14640e3
7d67a69
 
 
 
 
 
 
 
 
51c5a22
 
 
 
 
 
7d67a69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14640e3
 
 
 
7d67a69
14640e3
 
 
 
 
 
 
 
 
 
 
7d67a69
 
51c5a22
 
 
 
 
 
 
 
 
 
 
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
# 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 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, df_all, 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=pd.DataFrame(log_history),
                    headers=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"],
                    datatype=["str", "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.Accordion("▼ 惡意操作測試區", open=True):
                                        btn_hack_score = gr.Button("💀 嘗試偷改成績 (SQL Injection)", variant="stop", 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=pd.DataFrame(initial_abnormal) if initial_abnormal else pd.DataFrame(columns=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"]),
                                            headers=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"],
                                            datatype=["str", "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 df_all, 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"),
            (btn_hack_score, "malicious_sql_injection")
        ]

        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