BrianChuan commited on
Commit
fb04509
·
1 Parent(s): 51c5a22

完成階段三Vue 前端 - 個人 Log 檢視頁面開發與修正後端API錯誤

Browse files
.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ .env
2
+ SimHei*
3
+ __pycache__/
4
+ *.py[cod]
5
+ *$py.class
6
+ *.pkl
7
+ baseline_logs.csv
__pycache__/api.cpython-311.pyc CHANGED
Binary files a/__pycache__/api.cpython-311.pyc and b/__pycache__/api.cpython-311.pyc differ
 
__pycache__/auth.cpython-311.pyc CHANGED
Binary files a/__pycache__/auth.cpython-311.pyc and b/__pycache__/auth.cpython-311.pyc differ
 
__pycache__/models.cpython-311.pyc CHANGED
Binary files a/__pycache__/models.cpython-311.pyc and b/__pycache__/models.cpython-311.pyc differ
 
__pycache__/plotting.cpython-311.pyc CHANGED
Binary files a/__pycache__/plotting.cpython-311.pyc and b/__pycache__/plotting.cpython-311.pyc differ
 
__pycache__/ui.cpython-311.pyc CHANGED
Binary files a/__pycache__/ui.cpython-311.pyc and b/__pycache__/ui.cpython-311.pyc differ
 
api.py CHANGED
@@ -7,11 +7,16 @@ from fastapi.responses import JSONResponse
7
  from pydantic import BaseModel
8
  from typing import Optional, List
9
 
10
- from auth import simulate_click, USER_DB, check_session_timeouts, get_user_status
 
 
 
11
  from security import test_send_security_alert, verify_identity
12
  from config import SYSTEM_WEIGHTS, ACTION_MAP, FIXED_TEST_EMAIL
13
  from plotting import get_plot_data_for_api
14
  import pandas as pd
 
 
15
 
16
  api_app = FastAPI(title="數位保鏢 API")
17
 
@@ -23,6 +28,15 @@ class WeightsRequest(BaseModel):
23
  dw: float
24
  gw: float
25
  fw: float
 
 
 
 
 
 
 
 
 
26
 
27
  class StudentActionRequest(BaseModel):
28
  ip: str
@@ -40,12 +54,12 @@ class GenericActionRequest(StudentActionRequest):
40
  # --- 純淨 API 端點專用函式 (供內部呼叫) ---
41
  def internal_get_stats():
42
  """取得系統狀態與權重"""
43
- total = global_stats["total_count"]
44
- abnormal = global_stats["abnormal_count"]
45
  rate = round((abnormal / total * 100), 2) if total > 0 else 0
46
  return {
47
  "status": "success",
48
- "ai_ready": AI_READY,
49
  "data": {
50
  "total_logs": total,
51
  "abnormal_logs": abnormal,
@@ -58,10 +72,18 @@ def internal_get_logs(limit=20):
58
  """取得最新的 N 筆 Log"""
59
  return {
60
  "status": "success",
61
- "count": len(log_history[:limit]),
62
- "data": log_history[:limit]
63
  }
64
 
 
 
 
 
 
 
 
 
65
  # --- FastAPI 路由定義 ---
66
 
67
  @api_app.get("/api/stats")
@@ -72,18 +94,35 @@ def get_stats():
72
  def get_logs(limit: int = 20):
73
  return internal_get_logs(limit)
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  @api_app.get("/api/chart_data")
76
  def get_chart_data(action_name: str = Query(...)):
77
  """取得特定系統的圖表原始數據"""
78
- if not AI_READY:
79
- return {"status": "error", "message": "AI 模型未載入"}
80
 
81
- action_code = ACTION_MAP.get(action_name)
82
- if not action_code:
83
- return {"status": "error", "message": f"找不到系統: {action_name}"}
 
 
 
 
84
 
85
- df_hist = baseline_df[baseline_df['action'] == action_code].copy()
86
- df_live = pd.DataFrame(log_history)
87
 
88
  if not df_live.empty:
89
  df_live['timestamp'] = pd.to_datetime(df_live['timestamp'], errors='coerce')
@@ -116,16 +155,100 @@ def get_chart_data(action_name: str = Query(...)):
116
  @api_app.get("/api/abnormal_logs")
117
  def get_abnormal_logs():
118
  """取得所有異常 Log"""
119
- abnormal_logs = [log for log in log_history if "異常" in str(log.get("status", "")) or "錯誤" in str(log.get("status", "")) or "受限" in str(log.get("status", ""))]
120
  return {
121
  "status": "success",
122
  "count": len(abnormal_logs),
123
  "data": abnormal_logs
124
  }
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  @api_app.get("/api/plot_image")
127
  def get_plot_image(action_name: str = Query(...)):
128
  """取得圖表的 Base64 圖片"""
 
 
129
  img_data = get_plot_data_for_api(action_name)
130
  return {"status": "success", "image": img_data}
131
 
 
7
  from pydantic import BaseModel
8
  from typing import Optional, List
9
 
10
+ from auth import (
11
+ simulate_click, USER_DB, check_session_timeouts, get_user_status,
12
+ internal_process_login, internal_logout
13
+ )
14
  from security import test_send_security_alert, verify_identity
15
  from config import SYSTEM_WEIGHTS, ACTION_MAP, FIXED_TEST_EMAIL
16
  from plotting import get_plot_data_for_api
17
  import pandas as pd
18
+ import models # 使用 models.XXX 存取即時狀態,避免布林值被複製後失效
19
+ from utils import get_geo_level
20
 
21
  api_app = FastAPI(title="數位保鏢 API")
22
 
 
28
  dw: float
29
  gw: float
30
  fw: float
31
+
32
+ class LoginRequest(BaseModel):
33
+ account: str
34
+ password: str
35
+ captcha: str
36
+ ip: str
37
+ cookie: str
38
+ time_mode: str
39
+ custom_time: Optional[str] = ""
40
 
41
  class StudentActionRequest(BaseModel):
42
  ip: str
 
54
  # --- 純淨 API 端點專用函式 (供內部呼叫) ---
55
  def internal_get_stats():
56
  """取得系統狀態與權重"""
57
+ total = models.global_stats["total_count"]
58
+ abnormal = models.global_stats["abnormal_count"]
59
  rate = round((abnormal / total * 100), 2) if total > 0 else 0
60
  return {
61
  "status": "success",
62
+ "ai_ready": models.AI_READY,
63
  "data": {
64
  "total_logs": total,
65
  "abnormal_logs": abnormal,
 
72
  """取得最新的 N 筆 Log"""
73
  return {
74
  "status": "success",
75
+ "count": len(models.log_history[:limit]),
76
+ "data": models.log_history[:limit]
77
  }
78
 
79
+ def _extract_gr_update_value(upd):
80
+ """從 Gradio update 物件中提取 value (如果有)"""
81
+ if hasattr(upd, 'value'):
82
+ return upd.value
83
+ if isinstance(upd, dict) and 'value' in upd:
84
+ return upd['value']
85
+ return ""
86
+
87
  # --- FastAPI 路由定義 ---
88
 
89
  @api_app.get("/api/stats")
 
94
  def get_logs(limit: int = 20):
95
  return internal_get_logs(limit)
96
 
97
+ @api_app.post("/api/login")
98
+ def login_endpoint(req: LoginRequest):
99
+ return internal_process_login(
100
+ req.ip, req.cookie, req.account, req.password,
101
+ req.captcha, req.time_mode, req.custom_time
102
+ )
103
+
104
+ @api_app.post("/api/logout")
105
+ def logout_endpoint(req: StudentActionExtendedRequest):
106
+ return internal_logout(
107
+ req.ip, req.cookie, req.account, req.time_mode, req.custom_time
108
+ )
109
+
110
  @api_app.get("/api/chart_data")
111
  def get_chart_data(action_name: str = Query(...)):
112
  """取得特定系統的圖表原始數據"""
113
+ if not models.DATA_READY:
114
+ return {"status": "error", "message": "歷史基準資料未載入,請確認 baseline_logs.csv 是否存在或雲端同步是否正常"}
115
 
116
+ # 嘗試從顯示名稱映射到代碼,如果找不到,假設輸入已經是代碼
117
+ action_code = ACTION_MAP.get(action_name) or action_name
118
+
119
+ # 驗證 action_code 是否有效 (在 baseline_df 中存在或在 ACTION_MAP 的值中)
120
+ valid_codes = set(ACTION_MAP.values())
121
+ if action_code not in valid_codes and action_code not in models.baseline_df['action'].unique():
122
+ return {"status": "error", "message": f"找不到系統代碼或名稱: {action_name}"}
123
 
124
+ df_hist = models.baseline_df[models.baseline_df['action'] == action_code].copy()
125
+ df_live = pd.DataFrame(models.log_history)
126
 
127
  if not df_live.empty:
128
  df_live['timestamp'] = pd.to_datetime(df_live['timestamp'], errors='coerce')
 
155
  @api_app.get("/api/abnormal_logs")
156
  def get_abnormal_logs():
157
  """取得所有異常 Log"""
158
+ abnormal_logs = [log for log in models.log_history if "異常" in str(log.get("status", "")) or "錯誤" in str(log.get("status", "")) or "受限" in str(log.get("status", ""))]
159
  return {
160
  "status": "success",
161
  "count": len(abnormal_logs),
162
  "data": abnormal_logs
163
  }
164
 
165
+ @api_app.get("/api/get_user_logs")
166
+ def get_user_logs(session_id: str = Query(...)):
167
+ """
168
+ 獲取指定 session_id (cookie_id) 的所有日誌
169
+ 過濾自 Hugging Face 同步的歷史數據與當前記憶體數據
170
+ """
171
+ # 1. 準備過濾後的數據
172
+ filtered_logs = []
173
+
174
+ # 從歷史數據中過濾 (如果有的話)
175
+ if models.baseline_df is not None and not models.baseline_df.empty:
176
+ # 確保資料格式一致
177
+ df_filtered = models.baseline_df[models.baseline_df['cookie_id'] == session_id].copy()
178
+ if not df_filtered.empty:
179
+ df_filtered['timestamp'] = pd.to_datetime(df_filtered['timestamp']).dt.strftime('%Y-%m-%d %H:%M:%S')
180
+ filtered_logs.extend(df_filtered.to_dict('records'))
181
+
182
+ # 從當前記憶體日誌中過濾
183
+ mem_logs = [log for log in models.log_history if log.get("cookie_id") == session_id]
184
+ filtered_logs.extend(mem_logs)
185
+
186
+ # 2. 去重並排序 (依時間倒序)
187
+ # 使用 timestamp + action 作為簡單的去重基準
188
+ seen = set()
189
+ unique_logs = []
190
+ for log in filtered_logs:
191
+ key = (log.get('timestamp'), log.get('action'))
192
+ if key not in seen:
193
+ seen.add(key)
194
+ unique_logs.append(log)
195
+
196
+ unique_logs.sort(key=lambda x: x.get('timestamp', ''), reverse=True)
197
+
198
+ # 3. 豐富化資料 (加上地理位置、設備資訊、判定狀態)
199
+ final_logs = []
200
+ for log in unique_logs:
201
+ ip = log.get('ip_address', 'Unknown')
202
+ geo_code = get_geo_level(ip)
203
+
204
+ # 地理位置映射
205
+ geo_map = {
206
+ 0: "台北市 (校內專網)",
207
+ 1: "台北市 (校園網路)",
208
+ 2: "台灣 (宿舍/寬頻)",
209
+ 3: "台灣 (公共網路)",
210
+ 4: "美國 (海外連線)",
211
+ 5: "未知區域"
212
+ }
213
+ location = geo_map.get(geo_code, "未知區域")
214
+
215
+ # 設備資訊 (目前資料集無此欄位,給予模擬值或依 IP 推斷)
216
+ # 這裡模擬一些常見設備
217
+ device = "PC (Chrome/Windows)" if "140.124" in ip else "Mobile (Safari/iOS)"
218
+
219
+ # AI 判定狀態轉為中文
220
+ status_raw = str(log.get('status', 'success'))
221
+ ai_status = "異常" if ("異常" in status_raw or "受限" in status_raw or "錯誤" in status_raw) else "正常"
222
+
223
+ final_logs.append({
224
+ "time": log.get('timestamp'),
225
+ "action": log.get('action'),
226
+ "ip": ip,
227
+ "location": location,
228
+ "device": device,
229
+ "ai_status": ai_status,
230
+ "status_detail": status_raw # 保留原始詳細資訊
231
+ })
232
+
233
+ return {
234
+ "status": "success",
235
+ "session_id": session_id,
236
+ "count": len(final_logs),
237
+ "data": final_logs
238
+ }
239
+
240
+ @api_app.get("/api/chart_html")
241
+ def get_chart_html(action_name: str = Query(...)):
242
+ """取得圖表的 HTML 內容 (Gradio 專用)"""
243
+ from plotting import get_chart_js_html
244
+ html_data = get_chart_js_html(action_name)
245
+ return {"status": "success", "html": html_data}
246
+
247
  @api_app.get("/api/plot_image")
248
  def get_plot_image(action_name: str = Query(...)):
249
  """取得圖表的 Base64 圖片"""
250
+ if not models.DATA_READY:
251
+ return {"status": "error", "message": "歷史基準資料未載入"}
252
  img_data = get_plot_data_for_api(action_name)
253
  return {"status": "success", "image": img_data}
254
 
app.py CHANGED
@@ -23,7 +23,7 @@ api_app.add_middleware(
23
  allow_origins=["*"],
24
  allow_methods=["*"],
25
  allow_headers=["*"],
26
- allow_credentials=True,
27
  )
28
 
29
  if __name__ == "__main__":
 
23
  allow_origins=["*"],
24
  allow_methods=["*"],
25
  allow_headers=["*"],
26
+ allow_credentials=False, # 避免與 allow_origins=["*"] 衝突
27
  )
28
 
29
  if __name__ == "__main__":
auth.py CHANGED
@@ -101,6 +101,36 @@ def process_login(ip, cookie, account, password, captcha, time_mode, custom_time
101
  df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "failed_password", time_mode, custom_time)
102
  return fail_response("密碼錯誤!", df_all, df_abn, inp_upd)
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  def fast_login(target_account, ip, cookie, time_mode, custom_time):
105
  """快速登入"""
106
  role, name = USER_DB[target_account][1], USER_DB[target_account][2]
 
101
  df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "failed_password", time_mode, custom_time)
102
  return fail_response("密碼錯誤!", df_all, df_abn, inp_upd)
103
 
104
+ def internal_process_login(ip, cookie, account, password, captcha, time_mode, custom_time):
105
+ """內部處理登入邏輯,回傳 JSON 友善格式"""
106
+ if account not in USER_DB:
107
+ df_all, df_abn, inp_upd = record_log(ip, cookie, account, "Unknown", "login_attempt", "failed_no_account", time_mode, custom_time)
108
+ return {"status": "error", "message": "帳號不存在!"}
109
+
110
+ role, name = USER_DB[account][1], USER_DB[account][2]
111
+
112
+ if captcha.upper() != "FEIK":
113
+ df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "failed_captcha", time_mode, custom_time)
114
+ return {"status": "error", "message": "驗證碼錯誤!"}
115
+
116
+ if USER_DB[account][0] == password:
117
+ df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "success", time_mode, custom_time)
118
+ return {
119
+ "status": "success",
120
+ "role": "admin" if role == "管理" else "student",
121
+ "name": name,
122
+ "message": "登入成功"
123
+ }
124
+ else:
125
+ df_all, df_abn, inp_upd = record_log(ip, cookie, account, role, "login_attempt", "failed_password", time_mode, custom_time)
126
+ return {"status": "error", "message": "密碼錯誤!"}
127
+
128
+ def internal_logout(ip, cookie, account, time_mode, custom_time):
129
+ """內部處理登出邏輯"""
130
+ role = USER_DB.get(account, ["", "Unknown"])[1]
131
+ record_log(ip, cookie, account, role, "logout", "success", time_mode, custom_time)
132
+ return {"status": "success", "message": "已登出"}
133
+
134
  def fast_login(target_account, ip, cookie, time_mode, custom_time):
135
  """快速登入"""
136
  role, name = USER_DB[target_account][1], USER_DB[target_account][2]
models.py CHANGED
@@ -16,6 +16,7 @@ le_action = None
16
  baseline_df = None
17
  baseline_X = None
18
  AI_READY = False
 
19
 
20
  # 全域統計與狀態變數
21
  global_stats = {
@@ -29,12 +30,10 @@ log_history = []
29
 
30
  def load_models_and_data():
31
  """載入模型和歷史資料"""
32
- global le_action, baseline_df, baseline_X, AI_READY, global_stats, log_history
33
 
 
34
  try:
35
- # 載入標籤編碼器
36
- le_action = joblib.load(MODEL_FILES["label_encoder"])
37
-
38
  print("📂 正在從雲端獲取最新資料 (Hugging Face API)...")
39
  try:
40
  import logger_service
@@ -64,12 +63,17 @@ def load_models_and_data():
64
  new_logs = display_df[['timestamp', 'ip_address', 'cookie_id', 'account', 'role', 'action', 'status']].to_dict('records')
65
  log_history.clear()
66
  log_history.extend(new_logs)
 
 
 
 
 
 
67
 
68
- # 處理異常資料
69
- abnormal_mask = baseline_df['status'].astype(str).str.contains("異常", na=False)
70
- if abnormal_mask.any():
71
- abn_df_temp = baseline_df[abnormal_mask].sort_values(by='timestamp', ascending=False).copy()
72
- abn_df_temp['timestamp'] = abn_df_temp['timestamp'].dt.strftime('%Y-%m-%d %H:%M:%S')
73
 
74
  # 準備特徵矩陣
75
  baseline_df['month'] = baseline_df['timestamp'].dt.month
@@ -90,16 +94,14 @@ def load_models_and_data():
90
  ))
91
 
92
  AI_READY = True
93
- print("✅ AI 模組與歷史基準資料載入成功!")
94
 
95
  except Exception as e:
96
  AI_READY = False
97
- global_stats["total_count"] = 0
98
- global_stats["abnormal_count"] = 0
99
  if "KeyError: 118" in str(e) or "version https://git-lfs" in str(e):
100
  print(f"⚠️ AI 模組載入失敗:偵測到 Git LFS 指標檔案。請確保已安裝 git-lfs 並執行 `git lfs pull` 下載實際模型檔。")
101
  else:
102
- print(f"⚠️ AI 模組載入失敗,請確認檔案。錯誤: {e}")
103
 
104
  def check_anomaly(ip, account, action, log_time):
105
  """核心異常偵測邏輯"""
 
16
  baseline_df = None
17
  baseline_X = None
18
  AI_READY = False
19
+ DATA_READY = False # 新增:標記資料是否載入成功
20
 
21
  # 全域統計與狀態變數
22
  global_stats = {
 
30
 
31
  def load_models_and_data():
32
  """載入模型和歷史資料"""
33
+ global le_action, baseline_df, baseline_X, AI_READY, DATA_READY, global_stats, log_history
34
 
35
+ # 1. 優先載入資料 (圖表顯示核心)
36
  try:
 
 
 
37
  print("📂 正在從雲端獲取最新資料 (Hugging Face API)...")
38
  try:
39
  import logger_service
 
63
  new_logs = display_df[['timestamp', 'ip_address', 'cookie_id', 'account', 'role', 'action', 'status']].to_dict('records')
64
  log_history.clear()
65
  log_history.extend(new_logs)
66
+
67
+ DATA_READY = True
68
+ print("✅ 歷史基準資料載入成功!")
69
+ except Exception as e:
70
+ DATA_READY = False
71
+ print(f"❌ 資料載入失敗: {e}")
72
 
73
+ # 2. 嘗試載入 AI 模型 (異常偵測核心)
74
+ try:
75
+ # 載入標籤編碼器
76
+ le_action = joblib.load(MODEL_FILES["label_encoder"])
 
77
 
78
  # 準備特徵矩陣
79
  baseline_df['month'] = baseline_df['timestamp'].dt.month
 
94
  ))
95
 
96
  AI_READY = True
97
+ print("✅ AI 模組載入成功!")
98
 
99
  except Exception as e:
100
  AI_READY = False
 
 
101
  if "KeyError: 118" in str(e) or "version https://git-lfs" in str(e):
102
  print(f"⚠️ AI 模組載入失敗:偵測到 Git LFS 指標檔案。請確保已安裝 git-lfs 並執行 `git lfs pull` 下載實際模型檔。")
103
  else:
104
+ print(f"⚠️ AI 模組載入失敗 (不影響基本圖表顯示)。錯誤: {e}")
105
 
106
  def check_anomaly(ip, account, action, log_time):
107
  """核心異常偵測邏輯"""
plotting.py CHANGED
@@ -94,7 +94,7 @@ def get_dashboard_html():
94
 
95
  def get_plot_data_for_api(selected_key):
96
  """API 專用版本,返回 Base64 圖片字符串"""
97
- if not selected_key or not models.AI_READY:
98
  return None
99
 
100
  action_code = ACTION_MAP.get(selected_key)
@@ -202,7 +202,7 @@ def get_plot_data_for_api(selected_key):
202
 
203
  def get_plot_data(selected_key):
204
  """給 Gradio 界面使用,返回 Figure 對象"""
205
- if not selected_key or not models.AI_READY:
206
  return None
207
 
208
  action_code = ACTION_MAP.get(selected_key)
@@ -275,4 +275,136 @@ def get_plot_data(selected_key):
275
 
276
  def timer_update(selected_key):
277
  """定時更新函數"""
278
- return get_dashboard_html(), get_plot_data(selected_key)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
  def get_plot_data_for_api(selected_key):
96
  """API 專用版本,返回 Base64 圖片字符串"""
97
+ if not selected_key or not models.DATA_READY:
98
  return None
99
 
100
  action_code = ACTION_MAP.get(selected_key)
 
202
 
203
  def get_plot_data(selected_key):
204
  """給 Gradio 界面使用,返回 Figure 對象"""
205
+ if not selected_key or not models.DATA_READY:
206
  return None
207
 
208
  action_code = ACTION_MAP.get(selected_key)
 
275
 
276
  def timer_update(selected_key):
277
  """定時更新函數"""
278
+ return get_dashboard_html(), get_plot_data(selected_key)
279
+ def get_chart_js_html(selected_key):
280
+ """生成 Chart.js HTML 內容 (Gradio 專用)"""
281
+ import models
282
+ import numpy as np
283
+ import json
284
+ from config import ACTION_MAP
285
+ import pandas as pd
286
+
287
+ if not selected_key or not models.DATA_READY:
288
+ return f'<div style="text-align:center; padding:40px; color:#94a3b8;">【{selected_key}】系統目前尚無資料</div>'
289
+
290
+ # 取得代碼
291
+ action_code = ACTION_MAP.get(selected_key)
292
+ print(f"🔍 [Chart Debug] Selected: '{selected_key}', Mapped Code: '{action_code}'")
293
+
294
+ if not action_code:
295
+ # 嘗試模糊匹配
296
+ for k, v in ACTION_MAP.items():
297
+ if selected_key in k or k in selected_key:
298
+ action_code = v
299
+ print(f"💡 [Chart Debug] Fuzzy matched to: '{action_code}'")
300
+ break
301
+
302
+ if not action_code:
303
+ action_code = selected_key # 最後手段:直接用名稱查
304
+
305
+ # 取得資料
306
+ df_hist = models.baseline_df[models.baseline_df['action'] == action_code].copy()
307
+ print(f"📊 [Chart Debug] Found {len(df_hist)} historical records for '{action_code}'")
308
+
309
+ df_live = pd.DataFrame(models.log_history)
310
+ if not df_live.empty:
311
+ df_live['timestamp'] = pd.to_datetime(df_live['timestamp'], errors='coerce')
312
+ df_live = df_live.dropna(subset=['timestamp'])
313
+ df_live = df_live[df_live['action'] == action_code].copy()
314
+ print(f"📈 [Chart Debug] Found {len(df_live)} live records for '{action_code}'")
315
+
316
+ if not df_live.empty:
317
+ df_combined = pd.concat([df_hist, df_live], ignore_index=True)
318
+ df_combined = df_combined.drop_duplicates(subset=['timestamp', 'account', 'action'])
319
+ else:
320
+ df_combined = df_hist.copy()
321
+
322
+ if df_combined.empty:
323
+ print(f"⚠️ [Chart Debug] Combined data is empty for '{action_code}'")
324
+ return f'<div style="text-align:center; padding:40px; color:#94a3b8;">【{selected_key}】系統目前尚無資料</div>'
325
+
326
+ df_combined['month'] = df_combined['timestamp'].dt.month
327
+ df_combined['time_of_day'] = df_combined['timestamp'].dt.hour + df_combined['timestamp'].dt.minute / 60.0
328
+
329
+ is_abnormal = df_combined['status'].astype(str).str.contains("異常", na=False)
330
+
331
+ # 準備資料點 (添加 Jitter)
332
+ normal_pts = []
333
+ for _, row in df_combined[~is_abnormal].iterrows():
334
+ normal_pts.append({"x": float(row['month']) + (np.random.rand()-0.5)*0.4, "y": float(row['time_of_day']) + (np.random.rand()-0.5)*0.5})
335
+
336
+ abnormal_pts = []
337
+ for _, row in df_combined[is_abnormal].iterrows():
338
+ abnormal_pts.append({"x": float(row['month']) + (np.random.rand()-0.5)*0.4, "y": float(row['time_of_day']) + (np.random.rand()-0.5)*0.5})
339
+
340
+ normal_json = json.dumps(normal_pts)
341
+ abnormal_json = json.dumps(abnormal_pts)
342
+
343
+ # 使用隨機 ID 避免 Gradio 多次渲染衝突
344
+ import time
345
+ chart_id = f"chart_{int(time.time() * 1000)}"
346
+
347
+ html = f"""
348
+ <div style="width: 100%; height: 450px; background: white; border-radius: 12px; padding: 20px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); margin-top: 10px;">
349
+ <canvas id="{chart_id}"></canvas>
350
+ <script>
351
+ (function() {{
352
+ const initChart = () => {{
353
+ const canvas = document.getElementById('{chart_id}');
354
+ if (!canvas) return;
355
+ const ctx = canvas.getContext('2d');
356
+
357
+ if (typeof Chart === 'undefined') {{
358
+ setTimeout(initChart, 500);
359
+ return;
360
+ }}
361
+
362
+ console.log("Rendering Gradio Chart: {selected_key}");
363
+
364
+ new Chart(ctx, {{
365
+ type: 'scatter',
366
+ data: {{
367
+ datasets: [
368
+ {{
369
+ label: '正常行為',
370
+ data: {normal_json},
371
+ backgroundColor: 'rgba(16, 185, 129, 0.4)',
372
+ pointRadius: 5
373
+ }},
374
+ {{
375
+ label: '異常行為',
376
+ data: {abnormal_json},
377
+ backgroundColor: 'rgba(239, 68, 68, 0.8)',
378
+ pointRadius: 7,
379
+ pointStyle: 'rectRot'
380
+ }}
381
+ ]
382
+ }},
383
+ options: {{
384
+ responsive: true,
385
+ maintainAspectRatio: false,
386
+ animation: {{ duration: 800 }},
387
+ scales: {{
388
+ x: {{ title: {{ display: true, text: '月份' }}, min: 0.5, max: 12.5, ticks: {{ stepSize: 1 }} }},
389
+ y: {{ title: {{ display: true, text: '時間 (0-24h)' }}, min: 0, max: 24, reverse: true, ticks: {{ stepSize: 4 }} }}
390
+ }},
391
+ plugins: {{
392
+ legend: {{ position: 'top' }}
393
+ }}
394
+ }}
395
+ }});
396
+ }};
397
+
398
+ if (typeof Chart === 'undefined') {{
399
+ const script = document.createElement('script');
400
+ script.src = 'https://cdn.jsdelivr.net/npm/chart.js';
401
+ script.onload = initChart;
402
+ document.head.appendChild(script);
403
+ }} else {{
404
+ initChart();
405
+ }}
406
+ }})();
407
+ </script>
408
+ </div>
409
+ """
410
+ return html
ui.py CHANGED
@@ -81,33 +81,16 @@ def full_ui_update(selected_key):
81
  """更新所有 UI 元件:儀表板、圖表、所有 Log、異常 Log"""
82
  # 1. 獲取儀表板 HTML
83
  dash_html = get_dashboard_html()
84
-
85
- # 2. 獲取圖表 (透過 API 呼叫模擬)
86
- import requests
87
- from config import SERVER_CONFIG
88
- plot_img = None
89
- if selected_key:
90
- try:
91
- # 呼叫本機 API 端點
92
- port = SERVER_CONFIG["port"]
93
- api_url = f"http://127.0.0.1:{port}/api/plot_image?action_name={selected_key}"
94
- resp = requests.get(api_url, timeout=5)
95
- if resp.status_code == 200:
96
- img_base64 = resp.json().get("image")
97
- if img_base64:
98
- plot_img = f'<img src="{img_base64}" style="width:100%; max-height:450px; object-fit:contain; border-radius:8px; box-shadow:0 4px 12px rgba(0,0,0,0.1);">'
99
- except:
100
- # Fallback: 直接呼叫函式
101
- img_b64 = get_plot_data_for_api(selected_key)
102
- if img_b64:
103
- plot_img = f'<img src="{img_b64}" style="width:100%; max-height:450px; object-fit:contain; border-radius:8px; box-shadow:0 4px 12px rgba(0,0,0,0.1);">'
104
-
105
  # 3. 獲取 Log 資料
106
  df_all = pd.DataFrame(log_history)
107
  abnormal_logs = [log for log in log_history if any(word in str(log.get("status", "")) for word in ["異常", "錯誤", "受限"])]
108
  df_abn = pd.DataFrame(abnormal_logs) if abnormal_logs else pd.DataFrame(columns=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"])
109
-
110
- return dash_html, plot_img, df_all, df_abn
111
 
112
  def create_demo():
113
  """創建 Gradio 應用"""
@@ -210,8 +193,8 @@ def create_demo():
210
  with gr.Tab("🛡️ 管理員專區", id="admin_tab"):
211
  stat_dashboard_display = gr.HTML(value=get_dashboard_html())
212
  with gr.Accordion("▼ 系統操作時間分佈圖 (即時監控)", open=True, elem_classes="weight-card"):
213
- action_dropdown = gr.Dropdown(choices=list(ACTION_MAP.keys()), label="選擇要檢視的系統資料分佈", value=None)
214
- action_plot = gr.HTML(label="圖表顯示區")
215
 
216
  with gr.Accordion("▼ 防護強度微調 (AI 敏感度)", open=True, elem_classes="weight-card"):
217
  gr.Markdown("<span style='color:gray; font-size:0.9em;'>動態防護已全面啟動:包含 HDBSCAN 空間異常偵測、IP 聲譽聯防,以及動態視窗頻率攔截。</span>")
 
81
  """更新所有 UI 元件:儀表板、圖表、所有 Log、異常 Log"""
82
  # 1. 獲取儀表板 HTML
83
  dash_html = get_dashboard_html()
84
+
85
+ # 2. 獲取圖表 (直接使用 matplotlib Figure,交給 gr.Plot 渲染)
86
+ plot_fig = get_plot_data(selected_key)
87
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  # 3. 獲取 Log 資料
89
  df_all = pd.DataFrame(log_history)
90
  abnormal_logs = [log for log in log_history if any(word in str(log.get("status", "")) for word in ["異常", "錯誤", "受限"])]
91
  df_abn = pd.DataFrame(abnormal_logs) if abnormal_logs else pd.DataFrame(columns=["timestamp", "ip_address", "cookie_id", "account", "role", "action", "status"])
92
+
93
+ return dash_html, plot_fig, df_all, df_abn
94
 
95
  def create_demo():
96
  """創建 Gradio 應用"""
 
193
  with gr.Tab("🛡️ 管理員專區", id="admin_tab"):
194
  stat_dashboard_display = gr.HTML(value=get_dashboard_html())
195
  with gr.Accordion("▼ 系統操作時間分佈圖 (即時監控)", open=True, elem_classes="weight-card"):
196
+ action_dropdown = gr.Dropdown(choices=list(ACTION_MAP.keys()), label="選擇要檢視的系統資料分佈", value=list(ACTION_MAP.keys())[0])
197
+ action_plot = gr.Plot(label="圖表顯示區")
198
 
199
  with gr.Accordion("▼ 防護強度微調 (AI 敏感度)", open=True, elem_classes="weight-card"):
200
  gr.Markdown("<span style='color:gray; font-size:0.9em;'>動態防護已全面啟動:包含 HDBSCAN 空間異常偵測、IP 聲譽聯防,以及動態視窗頻率攔截。</span>")