PhilipL commited on
Commit
eaf8d86
·
verified ·
1 Parent(s): eaf04af

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +73 -52
app.py CHANGED
@@ -38,7 +38,7 @@ USER_DB = {
38
 
39
  log_history = []
40
 
41
- # --- 2. 核心邏輯與真實 HDBSCAN 異常偵測 ---
42
  def check_anomaly(ip, action, log_time, overall_w, time_w, ip_w, dev_w):
43
  if not AI_READY: return "success"
44
  if action in ["login_attempt", "logout"]: return "success"
@@ -143,7 +143,6 @@ def record_log(ip, cookie, account, role, action, status, time_mode, custom_time
143
 
144
  return pd.DataFrame(log_history)
145
 
146
- # --- 3. 視角切換與登入控制邏輯 ---
147
  def fast_login(target_account, ip, cookie, time_mode, custom_time, ow, tw, iw, dw):
148
  role, name = USER_DB[target_account][1], USER_DB[target_account][2]
149
  df = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
@@ -151,7 +150,6 @@ def fast_login(target_account, ip, cookie, time_mode, custom_time, ow, tw, iw, d
151
  welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938 (⚡快速登入模式)"
152
  target_tab = "student_tab" if role == "學生" else "admin_tab"
153
 
154
- # 回傳切換畫面與選定的 Tab
155
  return gr.update(visible=False), gr.update(visible=True), gr.update(selected=target_tab), "", welcome_msg, df, target_account
156
 
157
  def process_login(ip, cookie, account, password, captcha, time_mode, custom_time, ow, tw, iw, dw):
@@ -172,31 +170,24 @@ def process_login(ip, cookie, account, password, captcha, time_mode, custom_time
172
  if USER_DB[account][0] == password:
173
  welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938"
174
  df = record_log(ip, cookie, account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
175
-
176
  target_tab = "student_tab" if role == "學生" else "admin_tab"
 
177
  return gr.update(visible=False), gr.update(visible=True), gr.update(selected=target_tab), "", welcome_msg, df
178
 
179
  df = record_log(ip, cookie, account, role, "login_attempt", "failed_password", time_mode, custom_time, ow, tw, iw, dw)
180
  return fail_response("密碼錯誤!", df)
181
 
182
- def toggle_role(current_account, ip, cookie, time_mode, custom_time, ow, tw, iw, dw):
183
- """單鍵切換身分邏輯"""
184
- # 判斷當前是誰,切換成另一個
185
- target_account = "student" if current_account == "admin" else "admin"
186
-
187
  role, name = USER_DB[target_account][1], USER_DB[target_account][2]
188
  df = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
189
 
190
- welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938 (⚡單鍵切換模式)"
191
- target_tab = "student_tab" if role == "學生" else "admin_tab"
192
 
193
  return gr.update(selected=target_tab), welcome_msg, df, target_account
194
 
195
- def logout(ip, cookie, account, time_mode, custom_time, ow, tw, iw, dw):
196
- role = USER_DB.get(account, ["", "Unknown"])[1]
197
- df = record_log(ip, cookie, account, role, "logout", "success", time_mode, custom_time, ow, tw, iw, dw)
198
- return gr.update(visible=True), gr.update(visible=False), "", "", "", "", df
199
-
200
  def simulate_click(ip, cookie, account, button_name, time_mode, custom_time, ow, tw, iw, dw):
201
  role = USER_DB.get(account, ["", "Unknown"])[1]
202
  return record_log(ip, cookie, account, role, button_name, "success", time_mode, custom_time, ow, tw, iw, dw)
@@ -205,6 +196,11 @@ def test_anomaly(ip, cookie, account, action_name, test_time_str, ow, tw, iw, dw
205
  role = USER_DB.get(account, ["", "Unknown"])[1]
206
  return record_log(ip, cookie, account, role, action_name, "success", "自訂時間 (模擬過去/未來)", test_time_str, ow, tw, iw, dw)
207
 
 
 
 
 
 
208
  def toggle_time_input(mode):
209
  return gr.update(visible=(mode == "自訂時間 (模擬過去/未來)"))
210
 
@@ -215,7 +211,7 @@ custom_css = """
215
  .ntut-title { color: #d9534f; font-size: 22px; font-weight: bold; text-align: center; margin-bottom: 20px;}
216
  .weight-card { padding: 10px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-top: 10px; }
217
 
218
- /* 🌟 新增:隱藏 Tabs 標題列的魔法 CSS */
219
  .headless-tabs > div:first-child { display: none !important; }
220
  .headless-tabs { border: none !important; background: transparent !important; }
221
  """
@@ -255,15 +251,15 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
255
  welcome_text = gr.Markdown(value="", elem_classes="text-right")
256
  logout_btn = gr.Button("登出", size="sm")
257
 
258
- # 🌟 單一切換按鈕放置區
259
  with gr.Row():
260
- btn_toggle_role = gr.Button("🔄 單鍵切換身分 (Student ↔ Admin)", size="sm", variant="secondary")
 
261
  gr.Markdown("---")
262
 
263
- # 🌟 加上 headless-tabs 的 CSS 類別
264
  with gr.Tabs(selected="student_tab", elem_classes="headless-tabs") as role_tabs:
265
-
266
- with gr.Tab("👨‍🎓 學生專區", id="student_tab"):
267
  with gr.Accordion("▼ 1. 教務系統", open=True):
268
  with gr.Row(): btn_eval = gr.Button("▶ 期末網路教學評量", size="sm"); btn_pre_select = gr.Button("▶ 期末網路預選系統", size="sm"); btn_add_drop = gr.Button("▶ 開學後加退選系統", size="sm")
269
  with gr.Row(): btn_istudy = gr.Button("▶ 北科i學園PLUS", size="sm"); btn_card = gr.Button("▶ 學生證掛失及補發系統", size="sm"); btn_course = gr.Button("▶ 課程系統", size="sm")
@@ -275,7 +271,7 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
275
  with gr.Accordion("▼ 惡意操作測試區", open=True):
276
  btn_hack_score = gr.Button("💀 嘗試偷改成績 (SQL Injection)", variant="stop", size="sm")
277
 
278
- with gr.Tab("🛡️ 管理員專區", id="admin_tab"):
279
  with gr.Accordion("▼ 防護強度微調 (AI 敏感度)", open=True, elem_classes="weight-card"):
280
  gr.Markdown("<span style='color:gray; font-size:0.9em;'>手動輸入 0~100 的數值。提高強度會增加誤報率,但能更有效攔截新型攻擊。</span>")
281
  with gr.Row():
@@ -290,33 +286,44 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
290
 
291
  gr.Markdown("#### 1. 選課相關系統 (具備強烈季節性)")
292
  with gr.Row():
293
- t_pre_sel_norm = gr.Button("✅ 正常: 期末預選 (06-13 11:00)", size="sm"); t_pre_sel_anom = gr.Button("⚠️ 異常: 期末預選 (10-15 13:00)", size="sm")
 
294
  with gr.Row():
295
- t_course_norm = gr.Button("✅ 正常: 課程系統 (02-25 07:50)", size="sm"); t_course_anom = gr.Button("⚠️ 異常: 課程系統 (07-15 05:00)", size="sm")
 
296
  with gr.Row():
297
- t_add_drop_norm = gr.Button("✅ 正常: 開學加退選 (02-18 14:00)", size="sm"); t_add_drop_anom = gr.Button("⚠️ 異常: 開學加退選 (05-15 12:00)", size="sm")
 
298
 
299
  gr.Markdown("#### 2. 期末與成績相關 (具備週期性)")
300
  with gr.Row():
301
- t_eval_norm = gr.Button("✅ 正常: 教學評量 (12-17 13:20)", size="sm"); t_eval_anom = gr.Button("⚠️ 異常: 教學評量 (03-15 03:00)", size="sm")
 
302
  with gr.Row():
303
- t_score_norm = gr.Button("✅ 正常: 成績查詢 (11-17 18:50)", size="sm"); t_score_anom = gr.Button("⚠️ 異常: 成績查詢 (02-15 03:00)", size="sm")
 
304
 
305
  gr.Markdown("#### 3. 校務與行政服務 (生活與常規)")
306
  with gr.Row():
307
- t_dorm_norm = gr.Button("✅ 正常: 宿舍抽籤 (07-21 23:10)", size="sm"); t_dorm_anom = gr.Button("⚠️ 異常: 宿舍抽籤 (11-15 16:00)", size="sm")
 
308
  with gr.Row():
309
- t_schol_norm = gr.Button("✅ 正常: 獎學金申請 (03-12 14:00)", size="sm"); t_schol_anom = gr.Button("⚠️ 異常: 獎學金申請 (08-15 02:00)", size="sm")
 
310
  with gr.Row():
311
- t_card_norm = gr.Button("✅ 正常: 學生證掛失 (10-23 13:40)", size="sm"); t_card_anom = gr.Button("⚠️ 異常: 學生證掛失 (08-10 03:00)", size="sm")
 
312
 
313
  gr.Markdown("#### 4. 學習與日常輔助系統 (具備作息規律)")
314
  with gr.Row():
315
- 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")
 
316
  with gr.Row():
317
- t_leave_norm = gr.Button("✅ 正常: 學生請假 (03-29 08:45)", size="sm"); t_leave_anom = gr.Button("⚠️ 異常: 學生請假 (08-15 13:00)", size="sm")
 
318
  with gr.Row():
319
- t_vdesk_norm = gr.Button("✅ 正常: 軟體雲/WebMail (03-22 18:00)", size="sm"); t_vdesk_anom = gr.Button("⚠️ 異常: 軟體雲/WebMail (08-10 00:00)", size="sm")
 
320
 
321
  # --- 綁定事件 ---
322
  time_mode.change(fn=toggle_time_input, inputs=time_mode, outputs=custom_time_input)
@@ -328,22 +335,22 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
328
  inputs=[mock_ip, mock_cookie, acc_input, pwd_input, captcha_input, time_mode, custom_time_input] + weight_inputs,
329
  outputs=[login_view, dashboard_view, role_tabs, login_msg, welcome_text, log_display]
330
  )
331
-
332
  logout_btn.click(
333
  fn=logout,
334
  inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input] + weight_inputs,
335
- outputs=[login_view, dashboard_view, acc_input, pwd_input, captcha_input, login_msg, log_display]
336
  )
337
 
338
  fast_inputs = [mock_ip, mock_cookie, time_mode, custom_time_input] + weight_inputs
339
-
340
- btn_fast_student.click(fn=lambda ip, c, tm, ct, ow, tw, iw, dw: fast_login("student", ip, c, tm, ct, ow, tw, iw, dw), inputs=fast_inputs, outputs=[login_view, dashboard_view, role_tabs, login_msg, welcome_text, log_display, acc_input])
341
- btn_fast_admin.click(fn=lambda ip, c, tm, ct, ow, tw, iw, dw: fast_login("admin", ip, c, tm, ct, ow, tw, iw, dw), inputs=fast_inputs, outputs=[login_view, dashboard_view, role_tabs, login_msg, welcome_text, log_display, acc_input])
342
 
343
- # 🌟 單鍵切換按鈕綁定
 
 
 
344
  btn_toggle_role.click(
345
  fn=toggle_role,
346
- inputs=[acc_input, mock_ip, mock_cookie, time_mode, custom_time_input] + weight_inputs,
347
  outputs=[role_tabs, welcome_text, log_display, acc_input]
348
  )
349
 
@@ -365,17 +372,31 @@ with gr.Blocks(title="模擬校園入口與資安防護系統", css=custom_css)
365
  )
366
 
367
  test_mapping = [
368
- (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"),
369
- (t_course_norm, "click_course_system", "2026-02-25 07:50:00"), (t_course_anom, "click_course_system", "2026-07-15 05:00:00"),
370
- (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"),
371
- (t_eval_norm, "click_eval_system", "2026-12-17 13:20:00"), (t_eval_anom, "click_eval_system", "2026-03-15 03:00:00"),
372
- (t_score_norm, "click_score_query", "2026-11-17 18:50:00"), (t_score_anom, "click_score_query", "2026-02-15 03:00:00"),
373
- (t_dorm_norm, "click_dorm_system", "2026-07-21 23:10:00"), (t_dorm_anom, "click_dorm_system", "2026-11-15 16:00:00"),
374
- (t_schol_norm, "click_scholarship", "2026-03-12 14:00:00"), (t_schol_anom, "click_scholarship", "2026-08-15 02:00:00"),
375
- (t_card_norm, "click_card_loss", "2026-10-23 13:40:00"), (t_card_anom, "click_card_loss", "2026-08-10 03:00:00"),
376
- (t_istudy_norm, "click_istudy", "2026-10-18 16:00:00"), (t_istudy_anom, "click_istudy", "2026-08-20 02:00:00"),
377
- (t_leave_norm, "click_leave_system", "2026-03-29 08:45:00"), (t_leave_anom, "click_leave_system", "2026-08-15 13:00:00"),
378
- (t_vdesk_norm, "click_vdesk", "2026-03-22 18:00:00"), (t_vdesk_anom, "click_vdesk", "2026-08-10 00:00:00")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  ]
380
 
381
  for btn, action_name, test_time_str in test_mapping:
 
38
 
39
  log_history = []
40
 
41
+ # --- 3. 核心邏輯與真實 HDBSCAN 異常偵測 ---
42
  def check_anomaly(ip, action, log_time, overall_w, time_w, ip_w, dev_w):
43
  if not AI_READY: return "success"
44
  if action in ["login_attempt", "logout"]: return "success"
 
143
 
144
  return pd.DataFrame(log_history)
145
 
 
146
  def fast_login(target_account, ip, cookie, time_mode, custom_time, ow, tw, iw, dw):
147
  role, name = USER_DB[target_account][1], USER_DB[target_account][2]
148
  df = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
 
150
  welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938 (⚡快速登入模式)"
151
  target_tab = "student_tab" if role == "學生" else "admin_tab"
152
 
 
153
  return gr.update(visible=False), gr.update(visible=True), gr.update(selected=target_tab), "", welcome_msg, df, target_account
154
 
155
  def process_login(ip, cookie, account, password, captcha, time_mode, custom_time, ow, tw, iw, dw):
 
170
  if USER_DB[account][0] == password:
171
  welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938"
172
  df = record_log(ip, cookie, account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
 
173
  target_tab = "student_tab" if role == "學生" else "admin_tab"
174
+
175
  return gr.update(visible=False), gr.update(visible=True), gr.update(selected=target_tab), "", welcome_msg, df
176
 
177
  df = record_log(ip, cookie, account, role, "login_attempt", "failed_password", time_mode, custom_time, ow, tw, iw, dw)
178
  return fail_response("密碼錯誤!", df)
179
 
180
+ def toggle_role(current_tab, ip, cookie, time_mode, custom_time, ow, tw, iw, dw):
181
+ # 根據目前的 Tab 自動判定要切換成誰
182
+ target_account = "admin" if current_tab == "student_tab" else "student"
 
 
183
  role, name = USER_DB[target_account][1], USER_DB[target_account][2]
184
  df = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
185
 
186
+ welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938 (⚡快速切換模式)"
187
+ target_tab = "admin_tab" if current_tab == "student_tab" else "student_tab"
188
 
189
  return gr.update(selected=target_tab), welcome_msg, df, target_account
190
 
 
 
 
 
 
191
  def simulate_click(ip, cookie, account, button_name, time_mode, custom_time, ow, tw, iw, dw):
192
  role = USER_DB.get(account, ["", "Unknown"])[1]
193
  return record_log(ip, cookie, account, role, button_name, "success", time_mode, custom_time, ow, tw, iw, dw)
 
196
  role = USER_DB.get(account, ["", "Unknown"])[1]
197
  return record_log(ip, cookie, account, role, action_name, "success", "自訂時間 (模擬過去/未來)", test_time_str, ow, tw, iw, dw)
198
 
199
+ def logout(ip, cookie, account, time_mode, custom_time, ow, tw, iw, dw):
200
+ role = USER_DB.get(account, ["", "Unknown"])[1]
201
+ df = record_log(ip, cookie, account, role, "logout", "success", time_mode, custom_time, ow, tw, iw, dw)
202
+ return gr.update(visible=True), gr.update(visible=False), gr.update(), "", "", "", "", df
203
+
204
  def toggle_time_input(mode):
205
  return gr.update(visible=(mode == "自訂時間 (模擬過去/未來)"))
206
 
 
211
  .ntut-title { color: #d9534f; font-size: 22px; font-weight: bold; text-align: center; margin-bottom: 20px;}
212
  .weight-card { padding: 10px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-top: 10px; }
213
 
214
+ /* 🌟 新增:隱藏 Tabs 標題列的語法 */
215
  .headless-tabs > div:first-child { display: none !important; }
216
  .headless-tabs { border: none !important; background: transparent !important; }
217
  """
 
251
  welcome_text = gr.Markdown(value="", elem_classes="text-right")
252
  logout_btn = gr.Button("登出", size="sm")
253
 
254
+ # 🌟 新增:單一切換按鈕
255
  with gr.Row():
256
+ btn_toggle_role = gr.Button("Student ↔ Admin", size="sm", variant="secondary")
257
+
258
  gr.Markdown("---")
259
 
260
+ # 🌟 修改:改用 Tabs 排版並掛上 headless-tabs 樣式
261
  with gr.Tabs(selected="student_tab", elem_classes="headless-tabs") as role_tabs:
262
+ with gr.Tab("student", id="student_tab"):
 
263
  with gr.Accordion("▼ 1. 教務系統", open=True):
264
  with gr.Row(): btn_eval = gr.Button("▶ 期末網路教學評量", size="sm"); btn_pre_select = gr.Button("▶ 期末網路預選系統", size="sm"); btn_add_drop = gr.Button("▶ 開學後加退選系統", size="sm")
265
  with gr.Row(): btn_istudy = gr.Button("▶ 北科i學園PLUS", size="sm"); btn_card = gr.Button("▶ 學生證掛失及補發系統", size="sm"); btn_course = gr.Button("▶ 課程系統", size="sm")
 
271
  with gr.Accordion("▼ 惡意操作測試區", open=True):
272
  btn_hack_score = gr.Button("💀 嘗試偷改成績 (SQL Injection)", variant="stop", size="sm")
273
 
274
+ with gr.Tab("admin", id="admin_tab"):
275
  with gr.Accordion("▼ 防護強度微調 (AI 敏感度)", open=True, elem_classes="weight-card"):
276
  gr.Markdown("<span style='color:gray; font-size:0.9em;'>手動輸入 0~100 的數值。提高強度會增加誤報率,但能更有效攔截新型攻擊。</span>")
277
  with gr.Row():
 
286
 
287
  gr.Markdown("#### 1. 選課相關系統 (具備強烈季節性)")
288
  with gr.Row():
289
+ t_pre_sel_norm = gr.Button("✅ 正常: 期末網路預選系統 (06-13 11:00)", size="sm")
290
+ t_pre_sel_anom = gr.Button("⚠️ 異常: 期末網路預選系統 (10-15 13:00)", size="sm")
291
  with gr.Row():
292
+ t_course_norm = gr.Button("✅ 正常: 課程系統 (02-25 07:50)", size="sm")
293
+ t_course_anom = gr.Button("⚠️ 異常: 課程系統 (07-15 05:00)", size="sm")
294
  with gr.Row():
295
+ t_add_drop_norm = gr.Button("✅ 正常: 開學後加退選系統 (02-18 14:00)", size="sm")
296
+ t_add_drop_anom = gr.Button("⚠️ 異常: 開學後加退選系統 (05-15 12:00)", size="sm")
297
 
298
  gr.Markdown("#### 2. 期末與成績相關 (具備週期性)")
299
  with gr.Row():
300
+ t_eval_norm = gr.Button("✅ 正常: 期末網路教學評量 (12-17 13:20)", size="sm")
301
+ t_eval_anom = gr.Button("⚠️ 異常: 期末網路教學評量 (03-15 03:00)", size="sm")
302
  with gr.Row():
303
+ t_score_norm = gr.Button("✅ 正常: 學業成績查詢系統 (11-17 18:50)", size="sm")
304
+ t_score_anom = gr.Button("⚠️ 異常: ��業成績查詢系統 (02-15 03:00)", size="sm")
305
 
306
  gr.Markdown("#### 3. 校務與行政服務 (生活與常規)")
307
  with gr.Row():
308
+ t_dorm_norm = gr.Button("✅ 正常: 學生宿舍登錄(抽籤)系統 (07-21 23:10)", size="sm")
309
+ t_dorm_anom = gr.Button("⚠️ 異常: 學生宿舍登錄(抽籤)系統 (11-15 16:00)", size="sm")
310
  with gr.Row():
311
+ t_schol_norm = gr.Button("✅ 正常: 獎助學金申請系統 (03-12 14:00)", size="sm")
312
+ t_schol_anom = gr.Button("⚠️ 異常: 獎助學金申請系統 (08-15 02:00)", size="sm")
313
  with gr.Row():
314
+ t_card_norm = gr.Button("✅ 正常: 學生證掛失 (10-23 13:40)", size="sm")
315
+ t_card_anom = gr.Button("⚠️ 異常: 學生證掛失 (08-10 03:00)", size="sm")
316
 
317
  gr.Markdown("#### 4. 學習與日常輔助系統 (具備作息規律)")
318
  with gr.Row():
319
+ t_istudy_norm = gr.Button("✅ 正常: 北科i學園PLUS (10-18 16:00)", size="sm")
320
+ t_istudy_anom = gr.Button("⚠️ 異常: 北科i學園PLUS (08-20 02:00)", size="sm")
321
  with gr.Row():
322
+ t_leave_norm = gr.Button("✅ 正常: 學生請假系統 (03-29 08:45)", size="sm")
323
+ t_leave_anom = gr.Button("⚠️ 異常: 學生請假系統 (08-15 13:00)", size="sm")
324
  with gr.Row():
325
+ t_vdesk_norm = gr.Button("✅ 正常: 軟體雲/WebMail (03-22 18:00)", size="sm")
326
+ t_vdesk_anom = gr.Button("⚠️ 異常: 軟體雲/WebMail (08-10 00:00)", size="sm")
327
 
328
  # --- 綁定事件 ---
329
  time_mode.change(fn=toggle_time_input, inputs=time_mode, outputs=custom_time_input)
 
335
  inputs=[mock_ip, mock_cookie, acc_input, pwd_input, captcha_input, time_mode, custom_time_input] + weight_inputs,
336
  outputs=[login_view, dashboard_view, role_tabs, login_msg, welcome_text, log_display]
337
  )
 
338
  logout_btn.click(
339
  fn=logout,
340
  inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input] + weight_inputs,
341
+ outputs=[login_view, dashboard_view, role_tabs, acc_input, pwd_input, captcha_input, login_msg, log_display]
342
  )
343
 
344
  fast_inputs = [mock_ip, mock_cookie, time_mode, custom_time_input] + weight_inputs
345
+ fast_outputs = [login_view, dashboard_view, role_tabs, login_msg, welcome_text, log_display, acc_input]
 
 
346
 
347
+ btn_fast_student.click(fn=lambda ip, c, tm, ct, ow, tw, iw, dw: fast_login("student", ip, c, tm, ct, ow, tw, iw, dw), inputs=fast_inputs, outputs=fast_outputs)
348
+ btn_fast_admin.click(fn=lambda ip, c, tm, ct, ow, tw, iw, dw: fast_login("admin", ip, c, tm, ct, ow, tw, iw, dw), inputs=fast_inputs, outputs=fast_outputs)
349
+
350
+ # 🌟 綁定單一切換按���,把 role_tabs 當作 input 傳入判斷當前狀態
351
  btn_toggle_role.click(
352
  fn=toggle_role,
353
+ inputs=[role_tabs, mock_ip, mock_cookie, time_mode, custom_time_input] + weight_inputs,
354
  outputs=[role_tabs, welcome_text, log_display, acc_input]
355
  )
356
 
 
372
  )
373
 
374
  test_mapping = [
375
+ (t_pre_sel_norm, "click_pre_select_system", "2026-06-13 11:00:00"),
376
+ (t_pre_sel_anom, "click_pre_select_system", "2026-10-15 13:00:00"),
377
+ (t_course_norm, "click_course_system", "2026-02-25 07:50:00"),
378
+ (t_course_anom, "click_course_system", "2026-07-15 05:00:00"),
379
+ (t_add_drop_norm, "click_add_drop_system", "2026-02-18 14:00:00"),
380
+ (t_add_drop_anom, "click_add_drop_system", "2026-05-15 12:00:00"),
381
+
382
+ (t_eval_norm, "click_eval_system", "2026-12-17 13:20:00"),
383
+ (t_eval_anom, "click_eval_system", "2026-03-15 03:00:00"),
384
+ (t_score_norm, "click_score_query", "2026-11-17 18:50:00"),
385
+ (t_score_anom, "click_score_query", "2026-02-15 03:00:00"),
386
+
387
+ (t_dorm_norm, "click_dorm_system", "2026-07-21 23:10:00"),
388
+ (t_dorm_anom, "click_dorm_system", "2026-11-15 16:00:00"),
389
+ (t_schol_norm, "click_scholarship", "2026-03-12 14:00:00"),
390
+ (t_schol_anom, "click_scholarship", "2026-08-15 02:00:00"),
391
+ (t_card_norm, "click_card_loss", "2026-10-23 13:40:00"),
392
+ (t_card_anom, "click_card_loss", "2026-08-10 03:00:00"),
393
+
394
+ (t_istudy_norm, "click_istudy", "2026-10-18 16:00:00"),
395
+ (t_istudy_anom, "click_istudy", "2026-08-20 02:00:00"),
396
+ (t_leave_norm, "click_leave_system", "2026-03-29 08:45:00"),
397
+ (t_leave_anom, "click_leave_system", "2026-08-15 13:00:00"),
398
+ (t_vdesk_norm, "click_vdesk", "2026-03-22 18:00:00"),
399
+ (t_vdesk_anom, "click_vdesk", "2026-08-10 00:00:00")
400
  ]
401
 
402
  for btn, action_name, test_time_str in test_mapping: