PhilipL commited on
Commit
4896435
·
unverified ·
1 Parent(s): 80bb829

information system interface update

Browse files
Files changed (1) hide show
  1. app.py +136 -197
app.py CHANGED
@@ -12,7 +12,6 @@ import seaborn as sns
12
  import matplotlib.font_manager as fm
13
 
14
  # --- 🌟 解決 Matplotlib 中文顯示問題 ---
15
- # 啟動時自動下載並套用 SimHei (黑體) 字型,避免圖表出現方塊豆腐字
16
  font_path = 'SimHei.ttf'
17
  if not os.path.exists(font_path):
18
  try:
@@ -58,7 +57,6 @@ try:
58
  le_action = joblib.load('le_action.pkl')
59
  le_ip = joblib.load('le_ip.pkl')
60
 
61
- # 🌟 啟動時一次性載入歷史資料,供後續繪圖快速讀取
62
  baseline_df = pd.read_csv('baseline_logs.csv')
63
  baseline_df['timestamp'] = pd.to_datetime(baseline_df['timestamp'])
64
 
@@ -142,28 +140,22 @@ def get_dashboard_html():
142
  </div>
143
  """
144
 
145
- # --- 🌟 完美整合 Seaborn 的圖表繪製函式 ---
146
  def get_plot_data(selected_key):
147
  if not selected_key:
148
  return None
149
 
150
  action_code = ACTION_MAP.get(selected_key)
151
  try:
152
- plt.close('all') # 釋放舊記憶體,確保系統不卡頓
153
-
154
- # 1. 直接從記憶體中提取歷史基準資料 (完全免除讀檔延遲)
155
  df_hist = baseline_df[baseline_df['action'] == action_code].copy()
156
 
157
- # 2. 從 log_history 抓出「剛產生的即時紀錄」
158
  df_live = pd.DataFrame(log_history)
159
  if not df_live.empty:
160
  df_live['timestamp'] = pd.to_datetime(df_live['timestamp'])
161
  df_live = df_live[df_live['action'] == action_code].copy()
162
 
163
- # 3. 完美合併:將歷史訓練資料與即時資料無縫接合
164
  if not df_live.empty:
165
  df_combined = pd.concat([df_hist, df_live], ignore_index=True)
166
- # 使用 timestamp 移除可能重複計算的資料
167
  df_combined = df_combined.drop_duplicates(subset=['timestamp', 'account', 'action'])
168
  else:
169
  df_combined = df_hist.copy()
@@ -174,25 +166,21 @@ def get_plot_data(selected_key):
174
  ax.text(0.5, 0.5, f"【{selected_key}】目前尚無資料", ha='center', va='center', fontsize=15)
175
  return fig
176
 
177
- # 4. 計算 X 軸(月份) 與 Y 軸(時間小數)
178
  df_combined['month'] = df_combined['timestamp'].dt.month
179
  df_combined['time_of_day'] = df_combined['timestamp'].dt.hour + df_combined['timestamp'].dt.minute / 60.0
180
 
181
- # 5. 精準分類:區分「綠色正常」與「紅色異常」
182
  is_abnormal = df_combined['status'].astype(str).str.contains("異常", na=False)
183
  df_normal = df_combined[~is_abnormal]
184
  df_abnormal = df_combined[is_abnormal]
185
 
186
- # 開始繪圖
187
  fig, ax = plt.subplots(figsize=(12, 6))
188
  sns.set_theme(style="whitegrid")
189
  if os.path.exists(font_path): plt.rc('font', family='SimHei')
190
  plt.rcParams['axes.unicode_minus'] = False
191
 
192
  has_data = False
193
- months_order = list(range(1, 13)) # 確保 1 到 12 月依序排列
194
 
195
- # 🟢 繪製正常紀錄 (綠色點)
196
  if not df_normal.empty:
197
  has_data = True
198
  sns.stripplot(
@@ -201,7 +189,6 @@ def get_plot_data(selected_key):
201
  order=months_order, ax=ax, label='正常資料'
202
  )
203
 
204
- # 🔴 繪製異常紀錄 (紅色大叉叉)
205
  if not df_abnormal.empty:
206
  has_data = True
207
  sns.stripplot(
@@ -214,15 +201,11 @@ def get_plot_data(selected_key):
214
  ax.text(0.5, 0.5, f"【{selected_key}】目前尚無資料", ha='center', va='center', fontsize=15)
215
  return fig
216
 
217
- # 🌟 已經將標題 ax.set_title 移除
218
  ax.set_xlabel('月份 (1 ~ 12月)', fontsize=12)
219
  ax.set_ylabel('時間 (24小時制)', fontsize=12)
220
-
221
- # 設定 Y 軸為 0~24 點並倒轉 (早上在上面,半夜在下面)
222
  ax.set_yticks(np.arange(0, 25, 2))
223
  ax.set_ylim(24.5, -0.5)
224
 
225
- # 整理圖例 (避免 Seaborn 重複生成標籤)
226
  handles, labels = ax.get_legend_handles_labels()
227
  by_label = dict(zip(labels, handles))
228
  if by_label:
@@ -234,11 +217,9 @@ def get_plot_data(selected_key):
234
  print(f"繪圖失敗: {e}")
235
  return None
236
 
237
- # --- 定時器觸發同步更新 ---
238
  def timer_update(selected_key):
239
  return get_dashboard_html(), get_plot_data(selected_key)
240
 
241
- # --- Groq AI Agent 呼叫函式 ---
242
  def explain_abnormal_log(log_text):
243
  if not log_text or log_text.strip() == "":
244
  return "⚠️ 請先提供異常 Log 資訊以進行分析。"
@@ -391,17 +372,19 @@ def record_log(ip, cookie, account, role, action, status, time_mode, custom_time
391
 
392
  return df_all, df_abnormal, input_update
393
 
 
394
  def fast_login(target_account, ip, cookie, time_mode, custom_time, ow, tw, iw, dw):
395
  role, name = USER_DB[target_account][1], USER_DB[target_account][2]
396
  df_all, df_abn, inp_upd = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
397
  welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938 (⚡快速登入模式)"
398
  target_tab = "student_tab" if role == "學生" else "admin_tab"
399
- return gr.update(visible=False), gr.update(visible=True), gr.update(selected=target_tab), "", welcome_msg, df_all, df_abn, inp_upd, target_account
 
400
 
401
  def process_login(ip, cookie, account, password, captcha, time_mode, custom_time, ow, tw, iw, dw):
402
  valid_captcha = "FEIK"
403
  def fail_response(msg, df_all, df_abn, inp_upd):
404
- return gr.update(visible=True), gr.update(visible=False), gr.update(), msg, gr.update(), df_all, df_abn, inp_upd
405
 
406
  if account not in USER_DB:
407
  df_all, df_abn, inp_upd = record_log(ip, cookie, account, "Unknown", "login_attempt", "failed_no_account", time_mode, custom_time, ow, tw, iw, dw)
@@ -416,7 +399,8 @@ def process_login(ip, cookie, account, password, captcha, time_mode, custom_time
416
  welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938"
417
  df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
418
  target_tab = "student_tab" if role == "學生" else "admin_tab"
419
- return gr.update(visible=False), gr.update(visible=True), gr.update(selected=target_tab), "", welcome_msg, df_all, df_abn, inp_upd
 
420
 
421
  df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "failed_password", time_mode, custom_time, ow, tw, iw, dw)
422
  return fail_response("密碼錯誤!", df_all, df_abn, inp_upd)
@@ -432,7 +416,8 @@ def toggle_role(current_account, ip, cookie, time_mode, custom_time, ow, tw, iw,
432
  def logout(ip, cookie, account, time_mode, custom_time, ow, tw, iw, dw):
433
  role = USER_DB.get(account, ["", "Unknown"])[1]
434
  df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "logout", "success", time_mode, custom_time, ow, tw, iw, dw)
435
- return gr.update(visible=True), gr.update(visible=False), "", "", "", "", df_all, df_abn, inp_upd
 
436
 
437
  def simulate_click(ip, cookie, account, button_name, time_mode, custom_time, ow, tw, iw, dw):
438
  role = USER_DB.get(account, ["", "Unknown"])[1]
@@ -453,75 +438,21 @@ custom_css = """
453
  .weight-card { padding: 10px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-top: 10px; }
454
  .headless-tabs > div:first-child { display: none !important; }
455
  .headless-tabs { border: none !important; background: transparent !important; }
456
-
457
- /* 🌟 新增:統計儀表板 CSS,嘗試重現圖片樣式 (已加入深色模式強制白底黑字覆蓋) */
458
- .stat-dashboard {
459
- display: flex;
460
- gap: 15px;
461
- margin-bottom: 20px;
462
- justify-content: space-between;
463
- }
464
- .stat-card {
465
- flex: 1;
466
- background-color: #ffffff !important; /* 強制卡片底色為白色 */
467
- padding: 15px;
468
- border-radius: 10px;
469
- box-shadow: 0 4px 6px rgba(0,0,0,0.05);
470
- display: flex;
471
- align-items: center;
472
- gap: 15px;
473
- border-top: 4px solid transparent; /* 用於狀態卡的頂部顏色 */
474
- }
475
- .card-total { border-left: 4px solid #10b981; } /* Green left border */
476
- .card-anomaly { border-left: 4px solid #ef4444; } /* Red left border */
477
- .card-status { border-left: 4px solid #3b82f6; } /* Blue left border */
478
-
479
- .card-icon {
480
- font-size: 30px;
481
- width: 40px;
482
- text-align: center;
483
- }
484
- .card-content {
485
- flex: 1;
486
- }
487
- .card-label {
488
- color: #374151 !important; /* 強制標題文字為深灰色 */
489
- font-size: 13px;
490
- margin-bottom: 2px;
491
- font-weight: bold !important;
492
- }
493
- .card-value-container {
494
- display: flex;
495
- align-items: baseline;
496
- gap: 8px;
497
- }
498
- .card-value {
499
- font-size: 24px;
500
- font-weight: bold;
501
- }
502
- /* 針對「總監測量」的數字強制設為黑色,不影響異常率與狀態的專屬顏色 */
503
- .card-total .card-value {
504
- color: #000000 !important;
505
- }
506
- .card-unit {
507
- font-size: 14px;
508
- color: #4b5563 !important; /* 強制單位文字為深灰色 */
509
- font-weight: normal;
510
- }
511
- .card-trend {
512
- font-size: 12px;
513
- padding: 2px 6px;
514
- border-radius: 4px;
515
- }
516
- .trend-up {
517
- background-color: #fee2e2 !important;
518
- color: #ef4444 !important;
519
- }
520
- .card-sub-text {
521
- color: #6b7280 !important; /* 強制副標題說明文字為中灰色 */
522
- font-size: 11px;
523
- margin-top: 2px;
524
- }
525
  """
526
 
527
  with gr.Blocks(title="模擬校園入口與資安防護系統") as demo:
@@ -541,132 +472,140 @@ with gr.Blocks(title="模擬校園入口與資安防護系統") as demo:
541
  log_display = gr.Dataframe(value=log_history, headers=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"], datatype=["str", "str", "str", "str", "str", "str", "str"], interactive=False, wrap=True)
542
 
543
  with gr.Column(scale=2):
544
- with gr.Column(visible=True, elem_classes="login-container") as login_view:
545
- gr.Markdown("<div class='ntut-title'>校園入口網站 Taipei Tech Portal</div>")
546
- login_msg = gr.Markdown(value="", visible=True)
547
- with gr.Row(): acc_input = gr.Textbox(label="使用者帳號 (Account)", placeholder="student or admin")
548
- with gr.Row(): pwd_input = gr.Textbox(label="使用者密碼 (Password)", type="password", placeholder="password : 1234")
549
- 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>")
550
- with gr.Row(): captcha_input = gr.Textbox(label="請輸入驗證碼 (Keyin Code)")
551
- login_btn = gr.Button("登入 Login", variant="primary")
552
-
553
- gr.Markdown("---")
554
- gr.Markdown("<div style='text-align:center; color:gray; font-size: 0.9em;'>🛠️ 開發與測試專用捷徑</div>")
555
- with gr.Row():
556
- btn_fast_student = gr.Button("🚀 快速登入 (學生)", variant="secondary")
557
- btn_fast_admin = gr.Button("🚀 快速登入 (管理員)", variant="secondary")
558
-
559
- with gr.Column(visible=False) as dashboard_view:
560
- with gr.Row():
561
- gr.Markdown("### 資訊系統")
562
- welcome_text = gr.Markdown(value="", elem_classes="text-right")
563
- logout_btn = gr.Button("登出", size="sm")
564
-
565
- with gr.Row():
566
- btn_toggle_role = gr.Button("🔄 Student ↔ Admin", size="sm", variant="secondary")
567
- gr.Markdown("---")
568
 
569
- with gr.Tabs(selected="student_tab", elem_classes="headless-tabs") as role_tabs:
570
-
571
- with gr.Tab("👨‍🎓 學生專區", id="student_tab"):
572
- with gr.Accordion("▼ 1. 教務系統", open=True):
573
- with gr.Row(): btn_eval = gr.Button("▶ 期末網路教學評量", size="sm"); btn_pre_select = gr.Button("▶ 期末網路預選系統", size="sm"); btn_add_drop = gr.Button("▶ 開學後加退選系統", size="sm")
574
- with gr.Row(): btn_istudy = gr.Button("▶ 北科i學園PLUS", size="sm"); btn_card = gr.Button("▶ 學生證掛失及補發系統", size="sm"); btn_course = gr.Button("▶ 課程系統", size="sm")
575
- with gr.Row(): btn_score = gr.Button("▶ 學業成績查詢系統", size="sm")
576
- with gr.Accordion("▼ 2. 學務系統", open=True):
577
- with gr.Row(): btn_leave = gr.Button("▶ 學生請假系統", size="sm"); btn_dorm = gr.Button("▶ 學生宿舍登錄(抽籤)系統", size="sm"); btn_scholarship = gr.Button("▶ 獎助學金申請系統", size="sm")
578
- with gr.Accordion("▼ 3. 資訊服務", open=True):
579
- with gr.Row(): btn_webmail = gr.Button("▶ 網路郵局 WebMail", size="sm"); btn_vdesk = gr.Button("▶ 北科軟體雲", size="sm")
580
- with gr.Accordion("▼ 惡意操作測試區", open=True):
581
- btn_hack_score = gr.Button("💀 嘗試偷改成績 (SQL Injection)", variant="stop", size="sm")
582
-
583
- with gr.Tab("🛡️ 管理員專區", id="admin_tab"):
584
 
585
- stat_dashboard_display = gr.HTML(value=get_dashboard_html())
586
-
587
- with gr.Accordion("▼ 系統操作時間分佈圖 (即時監控)", open=True, elem_classes="weight-card"):
588
- gr.Markdown("<span style='color:gray; font-size:0.9em;'>選擇下方系統以檢視該系統在歷史與即時紀錄中的時間分佈狀況。圖表每分鐘會自動同步更新,歷史紀錄為綠點,即時攔截之異常為紅叉。</span>")
589
- action_dropdown = gr.Dropdown(
590
- choices=list(ACTION_MAP.keys()),
591
- label="選擇要檢視的系統資料分佈",
592
- value=None
593
- )
594
- action_plot = gr.Plot(show_label=False)
595
-
596
- with gr.Accordion("▼ 防護強度微調 (AI 敏感度)", open=True, elem_classes="weight-card"):
597
- gr.Markdown("<span style='color:gray; font-size:0.9em;'>手動輸入 0~100 的數值。提高強度會增加誤報率,但能更有效攔截新型攻擊。</span>")
598
- with gr.Row():
599
- overall_weight = gr.Number(label="總體防護強度 (%)", value=75, minimum=0, maximum=100)
600
- time_weight = gr.Number(label="時間特徵權重 (%)", value=80, minimum=0, maximum=100)
601
- with gr.Row():
602
- ip_weight = gr.Number(label="IP 聲譽權重 (%)", value=65, minimum=0, maximum=100)
603
- device_weight = gr.Number(label="裝置指紋/行為權重 (%)", value=45, minimum=0, maximum=100)
604
 
605
- with gr.Accordion("▼ 異常 Log 分析 (Groq AI Agent)", open=True, elem_classes="weight-card"):
606
- gr.Markdown("<span style='color:gray; font-size:0.9em;'>顯示所有被判定為異常的 Log。點擊分析可讓 AI 代為判讀潛在風險。</span>")
607
- abnormal_log_display = gr.Dataframe(
608
- value=initial_df_abn,
609
- headers=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"],
610
- datatype=["str", "str", "str", "str", "str", "str", "str"],
611
- interactive=False, wrap=True
612
- )
613
- with gr.Row():
614
- abnormal_log_input = gr.Textbox(label="要分析的異常 Log", placeholder="系統若偵測到新異常將會自動填入,您也可以手動修改...", scale=4)
615
- btn_explain_log = gr.Button("🧠 Groq AI 分析", variant="primary", scale=1)
616
-
617
- ai_explanation_output = gr.Textbox(label="AI 分析結果與說明", lines=4, interactive=False, placeholder="等待分析中...")
618
 
619
- with gr.Accordion("▼ HDBSCAN 異常辨識自動化測試區", open=True, elem_classes="weight-card"):
620
- gr.Markdown("<span style='color:gray; font-size:0.9em;'>點擊按鈕將直接以指定時間點執行該系統的操作,結果將顯示於左側 Log 區。</span>")
621
 
622
- gr.Markdown("#### 1. 選課相關系統 (具備強烈季節性)")
623
- with gr.Row():
624
- t_pre_sel_norm = gr.Button("✅ 正常: 期末網路預選系統 (06-13 11:00)", size="sm"); t_pre_sel_anom = gr.Button("⚠️ 異常: 期末網路預選系統 (10-15 13:00)", size="sm")
625
- with gr.Row():
626
- t_course_norm = gr.Button("✅ 正常: 課程系統 (02-25 07:50)", size="sm"); t_course_anom = gr.Button("⚠️ 異常: 課程系統 (07-15 05:00)", size="sm")
627
- with gr.Row():
628
- t_add_drop_norm = gr.Button("✅ 正常: 開學後加退選系統 (02-18 14:00)", size="sm"); t_add_drop_anom = gr.Button("⚠️ 異常: 開學後加退選系統 (05-15 12:00)", size="sm")
 
 
 
 
 
 
629
 
630
- gr.Markdown("#### 2. 期末與成績相關 (具備週期性)")
631
- with gr.Row():
632
- t_eval_norm = gr.Button("✅ 正常: 期末網路教學評量 (12-17 13:20)", size="sm"); t_eval_anom = gr.Button("⚠️ 異常: 期末網路教學評量 (03-15 03:00)", size="sm")
633
- with gr.Row():
634
- t_score_norm = gr.Button("✅ 正常: 學業成績查詢系統 (11-17 18:50)", size="sm"); t_score_anom = gr.Button("⚠️ 異常: 學業成績查詢系統 (02-15 03:00)", size="sm")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
635
 
636
- gr.Markdown("#### 3. 校務與行政服務 (生活與常規)")
637
- with gr.Row():
638
- t_dorm_norm = gr.Button("✅ 正常: 學生宿舍登錄(抽籤)系統 (07-21 23:10)", size="sm"); t_dorm_anom = gr.Button("⚠️ 異常: 學生宿舍登錄(抽籤)系統 (11-15 16:00)", size="sm")
639
- with gr.Row():
640
- t_schol_norm = gr.Button("✅ 正常: 獎助學金申請系統 (03-12 14:00)", size="sm"); t_schol_anom = gr.Button("⚠️ 異常: 獎助學金申請系統 (08-15 02:00)", size="sm")
641
- with gr.Row():
642
- t_card_norm = gr.Button("✅ 正常: 學生證掛失 (10-23 13:40)", size="sm"); t_card_anom = gr.Button("⚠️ 異常: 學生證掛失 (08-10 03:00)", size="sm")
 
 
 
 
 
 
643
 
644
- gr.Markdown("#### 4. 學習與日常輔助系統 (具備作息規律)")
645
- with gr.Row():
646
- 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")
647
- with gr.Row():
648
- t_leave_norm = gr.Button("✅ 正常: 學生請假系統 (03-29 08:45)", size="sm"); t_leave_anom = gr.Button("⚠️ 異常: 學生請假系統 (08-15 13:00)", size="sm")
649
- with gr.Row():
650
- t_vdesk_norm = gr.Button("✅ 正常: 軟體雲/WebMail (03-22 18:00)", size="sm"); t_vdesk_anom = gr.Button("⚠️ 異常: 軟體雲/WebMail (08-10 00:00)", size="sm")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
651
 
652
  # --- 綁定事件 ---
653
 
654
  demo.load(fn=timer_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot])
655
  dashboard_timer.tick(fn=timer_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot])
656
-
657
  action_dropdown.change(fn=get_plot_data, inputs=[action_dropdown], outputs=[action_plot])
658
-
659
  time_mode.change(fn=toggle_time_input, inputs=time_mode, outputs=custom_time_input)
660
  btn_explain_log.click(fn=explain_abnormal_log, inputs=[abnormal_log_input], outputs=[ai_explanation_output])
661
 
662
  weight_inputs = [overall_weight, time_weight, ip_weight, device_weight]
663
- update_outputs = [login_view, dashboard_view, role_tabs, login_msg, welcome_text, log_display, abnormal_log_display, abnormal_log_input]
 
 
664
 
665
  login_btn.click(
666
  fn=process_login, inputs=[mock_ip, mock_cookie, acc_input, pwd_input, captcha_input, time_mode, custom_time_input] + weight_inputs, outputs=update_outputs
667
  )
 
 
668
  logout_btn.click(
669
- fn=logout, inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input] + weight_inputs, outputs=[login_view, dashboard_view, acc_input, pwd_input, captcha_input, login_msg, log_display, abnormal_log_display, abnormal_log_input]
 
670
  )
671
 
672
  fast_inputs = [mock_ip, mock_cookie, time_mode, custom_time_input] + weight_inputs
 
12
  import matplotlib.font_manager as fm
13
 
14
  # --- 🌟 解決 Matplotlib 中文顯示問題 ---
 
15
  font_path = 'SimHei.ttf'
16
  if not os.path.exists(font_path):
17
  try:
 
57
  le_action = joblib.load('le_action.pkl')
58
  le_ip = joblib.load('le_ip.pkl')
59
 
 
60
  baseline_df = pd.read_csv('baseline_logs.csv')
61
  baseline_df['timestamp'] = pd.to_datetime(baseline_df['timestamp'])
62
 
 
140
  </div>
141
  """
142
 
 
143
  def get_plot_data(selected_key):
144
  if not selected_key:
145
  return None
146
 
147
  action_code = ACTION_MAP.get(selected_key)
148
  try:
149
+ plt.close('all')
 
 
150
  df_hist = baseline_df[baseline_df['action'] == action_code].copy()
151
 
 
152
  df_live = pd.DataFrame(log_history)
153
  if not df_live.empty:
154
  df_live['timestamp'] = pd.to_datetime(df_live['timestamp'])
155
  df_live = df_live[df_live['action'] == action_code].copy()
156
 
 
157
  if not df_live.empty:
158
  df_combined = pd.concat([df_hist, df_live], ignore_index=True)
 
159
  df_combined = df_combined.drop_duplicates(subset=['timestamp', 'account', 'action'])
160
  else:
161
  df_combined = df_hist.copy()
 
166
  ax.text(0.5, 0.5, f"【{selected_key}】目前尚無資料", ha='center', va='center', fontsize=15)
167
  return fig
168
 
 
169
  df_combined['month'] = df_combined['timestamp'].dt.month
170
  df_combined['time_of_day'] = df_combined['timestamp'].dt.hour + df_combined['timestamp'].dt.minute / 60.0
171
 
 
172
  is_abnormal = df_combined['status'].astype(str).str.contains("異常", na=False)
173
  df_normal = df_combined[~is_abnormal]
174
  df_abnormal = df_combined[is_abnormal]
175
 
 
176
  fig, ax = plt.subplots(figsize=(12, 6))
177
  sns.set_theme(style="whitegrid")
178
  if os.path.exists(font_path): plt.rc('font', family='SimHei')
179
  plt.rcParams['axes.unicode_minus'] = False
180
 
181
  has_data = False
182
+ months_order = list(range(1, 13))
183
 
 
184
  if not df_normal.empty:
185
  has_data = True
186
  sns.stripplot(
 
189
  order=months_order, ax=ax, label='正常資料'
190
  )
191
 
 
192
  if not df_abnormal.empty:
193
  has_data = True
194
  sns.stripplot(
 
201
  ax.text(0.5, 0.5, f"【{selected_key}】目前尚無資料", ha='center', va='center', fontsize=15)
202
  return fig
203
 
 
204
  ax.set_xlabel('月份 (1 ~ 12月)', fontsize=12)
205
  ax.set_ylabel('時間 (24小時制)', fontsize=12)
 
 
206
  ax.set_yticks(np.arange(0, 25, 2))
207
  ax.set_ylim(24.5, -0.5)
208
 
 
209
  handles, labels = ax.get_legend_handles_labels()
210
  by_label = dict(zip(labels, handles))
211
  if by_label:
 
217
  print(f"繪圖失敗: {e}")
218
  return None
219
 
 
220
  def timer_update(selected_key):
221
  return get_dashboard_html(), get_plot_data(selected_key)
222
 
 
223
  def explain_abnormal_log(log_text):
224
  if not log_text or log_text.strip() == "":
225
  return "⚠️ 請先提供異常 Log 資訊以進行分析。"
 
372
 
373
  return df_all, df_abnormal, input_update
374
 
375
+ # --- 🎯 路由更新邏輯:現在統一回傳 main_router 狀態 ---
376
  def fast_login(target_account, ip, cookie, time_mode, custom_time, ow, tw, iw, dw):
377
  role, name = USER_DB[target_account][1], USER_DB[target_account][2]
378
  df_all, df_abn, inp_upd = record_log(ip, cookie, target_account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
379
  welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938 (⚡快速登入模式)"
380
  target_tab = "student_tab" if role == "學生" else "admin_tab"
381
+ # 🌟 修改:切換至 dashboard_route 而不再修改 visible
382
+ return gr.update(selected="dashboard_route"), gr.update(selected=target_tab), "", welcome_msg, df_all, df_abn, inp_upd, target_account
383
 
384
  def process_login(ip, cookie, account, password, captcha, time_mode, custom_time, ow, tw, iw, dw):
385
  valid_captcha = "FEIK"
386
  def fail_response(msg, df_all, df_abn, inp_upd):
387
+ return gr.update(selected="login_route"), gr.update(), msg, gr.update(), df_all, df_abn, inp_upd
388
 
389
  if account not in USER_DB:
390
  df_all, df_abn, inp_upd = record_log(ip, cookie, account, "Unknown", "login_attempt", "failed_no_account", time_mode, custom_time, ow, tw, iw, dw)
 
399
  welcome_msg = f"**{name}** 歡迎您 | 線上人數: 938"
400
  df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "success", time_mode, custom_time, ow, tw, iw, dw)
401
  target_tab = "student_tab" if role == "學生" else "admin_tab"
402
+ # 🌟 修改:登入成功切換路由
403
+ return gr.update(selected="dashboard_route"), gr.update(selected=target_tab), "", welcome_msg, df_all, df_abn, inp_upd
404
 
405
  df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "failed_password", time_mode, custom_time, ow, tw, iw, dw)
406
  return fail_response("密碼錯誤!", df_all, df_abn, inp_upd)
 
416
  def logout(ip, cookie, account, time_mode, custom_time, ow, tw, iw, dw):
417
  role = USER_DB.get(account, ["", "Unknown"])[1]
418
  df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "logout", "success", time_mode, custom_time, ow, tw, iw, dw)
419
+ # 🌟 修改:登出時切回 login_route 路由
420
+ return gr.update(selected="login_route"), "", "", "", "", df_all, df_abn, inp_upd
421
 
422
  def simulate_click(ip, cookie, account, button_name, time_mode, custom_time, ow, tw, iw, dw):
423
  role = USER_DB.get(account, ["", "Unknown"])[1]
 
438
  .weight-card { padding: 10px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-top: 10px; }
439
  .headless-tabs > div:first-child { display: none !important; }
440
  .headless-tabs { border: none !important; background: transparent !important; }
441
+ .stat-dashboard { display: flex; gap: 15px; margin-bottom: 20px; justify-content: space-between; }
442
+ .stat-card { flex: 1; background-color: #ffffff !important; padding: 15px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); display: flex; align-items: center; gap: 15px; border-top: 4px solid transparent; }
443
+ .card-total { border-left: 4px solid #10b981; }
444
+ .card-anomaly { border-left: 4px solid #ef4444; }
445
+ .card-status { border-left: 4px solid #3b82f6; }
446
+ .card-icon { font-size: 30px; width: 40px; text-align: center; }
447
+ .card-content { flex: 1; }
448
+ .card-label { color: #374151 !important; font-size: 13px; margin-bottom: 2px; font-weight: bold !important; }
449
+ .card-value-container { display: flex; align-items: baseline; gap: 8px; }
450
+ .card-value { font-size: 24px; font-weight: bold; }
451
+ .card-total .card-value { color: #000000 !important; }
452
+ .card-unit { font-size: 14px; color: #4b5563 !important; font-weight: normal; }
453
+ .card-trend { font-size: 12px; padding: 2px 6px; border-radius: 4px; }
454
+ .trend-up { background-color: #fee2e2 !important; color: #ef4444 !important; }
455
+ .card-sub-text { color: #6b7280 !important; font-size: 11px; margin-top: 2px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  """
457
 
458
  with gr.Blocks(title="模擬校園入口與資安防護系統") as demo:
 
472
  log_display = gr.Dataframe(value=log_history, headers=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"], datatype=["str", "str", "str", "str", "str", "str", "str"], interactive=False, wrap=True)
473
 
474
  with gr.Column(scale=2):
475
+
476
+ # 🌟 核心修改:使用無頭標籤頁 (Headless Tabs) 做最外層的頁面路由
477
+ with gr.Tabs(selected="login_route", elem_classes="headless-tabs") as main_router:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
 
479
+ # 路由 1:登入畫面
480
+ with gr.Tab("Login", id="login_route"):
481
+ with gr.Column(elem_classes="login-container"):
482
+ gr.Markdown("<div class='ntut-title'>校園入口網站 Taipei Tech Portal</div>")
483
+ login_msg = gr.Markdown(value="", visible=True)
484
+ with gr.Row(): acc_input = gr.Textbox(label="使用者帳號 (Account)", placeholder="student or admin")
485
+ with gr.Row(): pwd_input = gr.Textbox(label="使用者密碼 (Password)", type="password", placeholder="password : 1234")
486
+ 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>")
487
+ with gr.Row(): captcha_input = gr.Textbox(label="請輸入驗證碼 (Keyin Code)")
488
+ login_btn = gr.Button("登入 Login", variant="primary")
 
 
 
 
 
489
 
490
+ gr.Markdown("---")
491
+ gr.Markdown("<div style='text-align:center; color:gray; font-size: 0.9em;'>🛠️ 開發與測試專用捷徑</div>")
492
+ with gr.Row():
493
+ btn_fast_student = gr.Button("🚀 快速登入 (學生)", variant="secondary")
494
+ btn_fast_admin = gr.Button("🚀 快速登入 (管理員)", variant="secondary")
495
+
496
+ # 路由 2:資訊系統儀表板 (不再使用 visible=False)
497
+ with gr.Tab("Dashboard", id="dashboard_route"):
498
+ with gr.Column():
499
+ with gr.Row():
500
+ gr.Markdown("### 資訊系統")
501
+ welcome_text = gr.Markdown(value="", elem_classes="text-right")
502
+ logout_btn = gr.Button("登出", size="sm")
 
 
 
 
 
 
503
 
504
+ with gr.Row():
505
+ btn_toggle_role = gr.Button("🔄 Student ↔ Admin", size="sm", variant="secondary")
506
+ gr.Markdown("---")
 
 
 
 
 
 
 
 
 
 
507
 
508
+ with gr.Tabs(selected="student_tab", elem_classes="headless-tabs") as role_tabs:
 
509
 
510
+ with gr.Tab("👨‍🎓 學生專區", id="student_tab"):
511
+ with gr.Accordion("▼ 1. 教務系統", open=True):
512
+ with gr.Row(): btn_eval = gr.Button("▶ 期末網路教學評量", size="sm"); btn_pre_select = gr.Button("▶ 期末網路預選系統", size="sm"); btn_add_drop = gr.Button("▶ 開學後加退選系統", size="sm")
513
+ with gr.Row(): btn_istudy = gr.Button("▶ 北科i學園PLUS", size="sm"); btn_card = gr.Button("▶ 學生證掛失及補發系統", size="sm"); btn_course = gr.Button("▶ 課程系統", size="sm")
514
+ with gr.Row(): btn_score = gr.Button("▶ 學業成績查詢系統", size="sm")
515
+ with gr.Accordion("▼ 2. 學務系統", open=True):
516
+ with gr.Row(): btn_leave = gr.Button("▶ 學生請假系統", size="sm"); btn_dorm = gr.Button("▶ 學生宿舍登錄(抽籤)系統", size="sm"); btn_scholarship = gr.Button("▶ 獎助學金申請系統", size="sm")
517
+ with gr.Accordion("▼ 3. 資訊服務", open=True):
518
+ with gr.Row(): btn_webmail = gr.Button("▶ 網路郵局 WebMail", size="sm"); btn_vdesk = gr.Button("▶ 北科軟體雲", size="sm")
519
+ with gr.Accordion("▼ 惡意操作測試區", open=True):
520
+ btn_hack_score = gr.Button("💀 嘗試偷改成績 (SQL Injection)", variant="stop", size="sm")
521
+
522
+ with gr.Tab("🛡️ 管理員專區", id="admin_tab"):
523
 
524
+ stat_dashboard_display = gr.HTML(value=get_dashboard_html())
525
+
526
+ with gr.Accordion("▼ 系統操作時間分佈圖 (即時監控)", open=True, elem_classes="weight-card"):
527
+ gr.Markdown("<span style='color:gray; font-size:0.9em;'>選擇下方系統以檢視該系統在歷史與即時紀錄中的時間分佈狀況。圖表每分鐘會自動同步更新,歷史紀錄為綠點,即時攔截之異常為紅叉。</span>")
528
+ action_dropdown = gr.Dropdown(
529
+ choices=list(ACTION_MAP.keys()),
530
+ label="選擇要檢視的系統資料分佈",
531
+ value=None
532
+ )
533
+ action_plot = gr.Plot(show_label=False)
534
+
535
+ with gr.Accordion("▼ 防護強度微調 (AI 敏感度)", open=True, elem_classes="weight-card"):
536
+ gr.Markdown("<span style='color:gray; font-size:0.9em;'>手動輸入 0~100 的數值。提高強度會增加誤報率,但能更有效攔截新型攻擊。</span>")
537
+ with gr.Row():
538
+ overall_weight = gr.Number(label="總體防護強度 (%)", value=75, minimum=0, maximum=100)
539
+ time_weight = gr.Number(label="時間特徵權重 (%)", value=80, minimum=0, maximum=100)
540
+ with gr.Row():
541
+ ip_weight = gr.Number(label="IP 聲譽權重 (%)", value=65, minimum=0, maximum=100)
542
+ device_weight = gr.Number(label="裝置指紋/行為權重 (%)", value=45, minimum=0, maximum=100)
543
 
544
+ with gr.Accordion("▼ 異常 Log 分析 (Groq AI Agent)", open=True, elem_classes="weight-card"):
545
+ gr.Markdown("<span style='color:gray; font-size:0.9em;'>顯示所有被判定為異常的 Log。點擊分析可讓 AI 代為判讀潛在風險。</span>")
546
+ abnormal_log_display = gr.Dataframe(
547
+ value=initial_df_abn,
548
+ headers=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"],
549
+ datatype=["str", "str", "str", "str", "str", "str", "str"],
550
+ interactive=False, wrap=True
551
+ )
552
+ with gr.Row():
553
+ abnormal_log_input = gr.Textbox(label="要分析的異常 Log", placeholder="系統若偵測到新異常將會自動填入,您也可以手動修改...", scale=4)
554
+ btn_explain_log = gr.Button("🧠 Groq AI 分析", variant="primary", scale=1)
555
+
556
+ ai_explanation_output = gr.Textbox(label="AI 分析結果與說明", lines=4, interactive=False, placeholder="等待分析中...")
557
 
558
+ # ... (原有的自動化測試區按鈕保留不變) ...
559
+ with gr.Accordion("▼ HDBSCAN 異常辨識自動化測試區", open=True, elem_classes="weight-card"):
560
+ gr.Markdown("<span style='color:gray; font-size:0.9em;'>點擊按鈕將直接以指定時間點執行該系統的操作,結果將顯示於左側 Log 區。</span>")
561
+ gr.Markdown("#### 1. 選課相關系統 (具備強烈季節性)")
562
+ with gr.Row():
563
+ t_pre_sel_norm = gr.Button("✅ 正常: 期末網路預選系統 (06-13 11:00)", size="sm"); t_pre_sel_anom = gr.Button("⚠️ 異常: 期末網路預選系統 (10-15 13:00)", size="sm")
564
+ with gr.Row():
565
+ t_course_norm = gr.Button("✅ 正常: 課程系統 (02-25 07:50)", size="sm"); t_course_anom = gr.Button("⚠️ 異常: 課程系統 (07-15 05:00)", size="sm")
566
+ with gr.Row():
567
+ t_add_drop_norm = gr.Button("✅ 正常: 開學後加退選系統 (02-18 14:00)", size="sm"); t_add_drop_anom = gr.Button("⚠️ 異常: 開學後加退選系統 (05-15 12:00)", size="sm")
568
+ gr.Markdown("#### 2. 期末與成績相關 (具備週期性)")
569
+ with gr.Row():
570
+ t_eval_norm = gr.Button("✅ 正常: 期末網路教學評量 (12-17 13:20)", size="sm"); t_eval_anom = gr.Button("⚠️ 異常: 期末網路教學評量 (03-15 03:00)", size="sm")
571
+ with gr.Row():
572
+ t_score_norm = gr.Button("✅ 正常: 學業成績查詢系統 (11-17 18:50)", size="sm"); t_score_anom = gr.Button("⚠️ 異常: 學業成績查詢系統 (02-15 03:00)", size="sm")
573
+ gr.Markdown("#### 3. 校務與行政服務 (生活與常規)")
574
+ with gr.Row():
575
+ t_dorm_norm = gr.Button("✅ 正常: 學生宿舍登錄(抽籤)系統 (07-21 23:10)", size="sm"); t_dorm_anom = gr.Button("⚠️ 異常: 學生宿舍登錄(抽籤)系統 (11-15 16:00)", size="sm")
576
+ with gr.Row():
577
+ t_schol_norm = gr.Button("✅ 正常: 獎助學金申請系統 (03-12 14:00)", size="sm"); t_schol_anom = gr.Button("⚠️ 異常: 獎助學金申請系統 (08-15 02:00)", size="sm")
578
+ with gr.Row():
579
+ t_card_norm = gr.Button("✅ 正常: 學生證掛失 (10-23 13:40)", size="sm"); t_card_anom = gr.Button("⚠️ 異常: 學生證掛失 (08-10 03:00)", size="sm")
580
+ gr.Markdown("#### 4. 學習與日常輔助系統 (具備作息規律)")
581
+ with gr.Row():
582
+ 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")
583
+ with gr.Row():
584
+ t_leave_norm = gr.Button("✅ 正常: 學生請假系統 (03-29 08:45)", size="sm"); t_leave_anom = gr.Button("⚠️ 異常: 學生請假系統 (08-15 13:00)", size="sm")
585
+ with gr.Row():
586
+ t_vdesk_norm = gr.Button("✅ 正常: 軟體雲/WebMail (03-22 18:00)", size="sm"); t_vdesk_anom = gr.Button("⚠️ 異常: 軟體雲/WebMail (08-10 00:00)", size="sm")
587
 
588
  # --- 綁定事件 ---
589
 
590
  demo.load(fn=timer_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot])
591
  dashboard_timer.tick(fn=timer_update, inputs=[action_dropdown], outputs=[stat_dashboard_display, action_plot])
 
592
  action_dropdown.change(fn=get_plot_data, inputs=[action_dropdown], outputs=[action_plot])
 
593
  time_mode.change(fn=toggle_time_input, inputs=time_mode, outputs=custom_time_input)
594
  btn_explain_log.click(fn=explain_abnormal_log, inputs=[abnormal_log_input], outputs=[ai_explanation_output])
595
 
596
  weight_inputs = [overall_weight, time_weight, ip_weight, device_weight]
597
+
598
+ # 🎯 這裡改成更新 main_router,而不是舊的 login_view, dashboard_view
599
+ update_outputs = [main_router, role_tabs, login_msg, welcome_text, log_display, abnormal_log_display, abnormal_log_input]
600
 
601
  login_btn.click(
602
  fn=process_login, inputs=[mock_ip, mock_cookie, acc_input, pwd_input, captcha_input, time_mode, custom_time_input] + weight_inputs, outputs=update_outputs
603
  )
604
+
605
+ # 登出時多清除原本帳密的輸入框
606
  logout_btn.click(
607
+ fn=logout, inputs=[mock_ip, mock_cookie, acc_input, time_mode, custom_time_input] + weight_inputs,
608
+ outputs=[main_router, acc_input, pwd_input, captcha_input, login_msg, log_display, abnormal_log_display, abnormal_log_input]
609
  )
610
 
611
  fast_inputs = [mock_ip, mock_cookie, time_mode, custom_time_input] + weight_inputs