KyrosDev Claude commited on
Commit
367e140
·
1 Parent(s): b5d5679

Code review optimizations and cleanup

Browse files

Major improvements:
- Remove redundant Streamlit app (1,479 lines deleted)
- Optimize configuration management with better error handling
- Improve API error handling consistency and security
- Add proper service layer abstraction for license operations
- Fix authentication configuration validation
- Add unified server for Hugging Face Spaces deployment
- Reduce total codebase by 25% while maintaining functionality

Technical changes:
- Enhanced config.js with loading states and better validation
- Improved auth.js configuration checks
- Added get_license_info method to service layer
- Standardized API error responses
- Updated Dockerfile for unified deployment

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

Dockerfile CHANGED
@@ -15,6 +15,7 @@ RUN pip install --no-cache-dir -r requirements.txt
15
  # 複製應用程式碼
16
  COPY app/ ./app/
17
  COPY frontend/ ./frontend/
 
18
  COPY start.sh .
19
 
20
  # 設定執行權限
 
15
  # 複製應用程式碼
16
  COPY app/ ./app/
17
  COPY frontend/ ./frontend/
18
+ COPY unified_server.py .
19
  COPY start.sh .
20
 
21
  # 設定執行權限
app/api/license.py CHANGED
@@ -151,27 +151,12 @@ async def delete_license(license_id: str):
151
  async def check_license_info(license_code: str):
152
  """檢查授權資訊 (不記錄使用日誌)"""
153
  try:
154
- # 簡單查詢授權資訊,記錄日誌
155
- result = license_service.supabase.table("licenses").select("*").eq(
156
- "license_code", license_code
157
- ).execute()
158
-
159
- if not result.data:
160
- return {"success": False, "message": "授權碼不存在"}
161
-
162
- license_data = result.data[0]
163
- return {
164
- "success": True,
165
- "data": {
166
- "license_code": license_data["license_code"],
167
- "user_name": license_data["user_name"],
168
- "user_email": license_data["user_email"],
169
- "hardware_id": license_data["hardware_id"],
170
- "expires_at": license_data["expires_at"],
171
- "is_active": license_data["is_active"],
172
- "activated_at": license_data["activated_at"],
173
- "last_used_at": license_data["last_used_at"]
174
- }
175
- }
176
  except Exception as e:
177
- raise HTTPException(status_code=500, detail=f"系統錯誤: {str(e)}")
 
151
  async def check_license_info(license_code: str):
152
  """檢查授權資訊 (不記錄使用日誌)"""
153
  try:
154
+ # 使用服務層方法而是直接存取資料庫
155
+ result = await license_service.get_license_info(license_code)
156
+ return APIResponse(
157
+ success=result["success"],
158
+ message=result["message"],
159
+ data=result.get("data")
160
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  except Exception as e:
162
+ raise HTTPException(status_code=500, detail="系統錯誤,請稍後重試")
app/services/license_service.py CHANGED
@@ -316,6 +316,35 @@ class LicenseService:
316
  print(f"刪除授權錯誤: {e}")
317
  return {"success": False, "message": f"刪除失敗: {str(e)}"}
318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  async def _log_usage(self, license_id: Optional[str], action: str, ip_address: Optional[str] = None,
320
  hardware_info: Optional[str] = None, error_message: Optional[str] = None):
321
  """記錄使用日誌"""
 
316
  print(f"刪除授權錯誤: {e}")
317
  return {"success": False, "message": f"刪除失敗: {str(e)}"}
318
 
319
+ async def get_license_info(self, license_code: str) -> Dict[str, Any]:
320
+ """取得授權資訊 (不記錄使用日誌)"""
321
+ try:
322
+ result = self.supabase.table("licenses").select("*").eq(
323
+ "license_code", license_code
324
+ ).execute()
325
+
326
+ if not result.data:
327
+ return {"success": False, "message": "授權碼不存在"}
328
+
329
+ license_data = result.data[0]
330
+ return {
331
+ "success": True,
332
+ "message": "查詢成功",
333
+ "data": {
334
+ "license_code": license_data["license_code"],
335
+ "user_name": license_data["user_name"],
336
+ "user_email": license_data["user_email"],
337
+ "hardware_id": license_data["hardware_id"],
338
+ "expires_at": license_data["expires_at"],
339
+ "is_active": license_data["is_active"],
340
+ "activated_at": license_data["activated_at"],
341
+ "last_used_at": license_data["last_used_at"]
342
+ }
343
+ }
344
+ except Exception as e:
345
+ print(f"查詢授權資訊錯誤: {e}")
346
+ return {"success": False, "message": "查詢失敗"}
347
+
348
  async def _log_usage(self, license_id: Optional[str], action: str, ip_address: Optional[str] = None,
349
  hardware_info: Optional[str] = None, error_message: Optional[str] = None):
350
  """記錄使用日誌"""
app/streamlit_app.py DELETED
@@ -1,1480 +0,0 @@
1
- """
2
- KSTools License Manager - 現代化 Streamlit Web 管理界面
3
- 優化版本 - 改善 UI/UX、性能和穩定性
4
- """
5
-
6
- import streamlit as st
7
- import pandas as pd
8
- import plotly.express as px
9
- import plotly.graph_objects as go
10
- from datetime import datetime, timedelta, timezone
11
- import requests
12
- import json
13
- import time
14
- from typing import Dict, List, Any, Optional
15
- import os
16
- from dataclasses import dataclass
17
- from enum import Enum
18
-
19
- # 設定
20
- class Config:
21
- """應用程式設定"""
22
- API_BASE = os.getenv("API_BASE", "http://localhost:8000/api")
23
- REQUEST_TIMEOUT = 10
24
- CACHE_TTL = 60 # 1分鐘快取 (減少頻繁請求)
25
- PAGE_SIZE = 20
26
-
27
- class Status(Enum):
28
- """狀態枚舉"""
29
- SUCCESS = "success"
30
- ERROR = "error"
31
- WARNING = "warning"
32
- INFO = "info"
33
-
34
- @dataclass
35
- class ApiResponse:
36
- """API 回應資料結構"""
37
- success: bool
38
- data: Any = None
39
- message: str = ""
40
- status_code: int = 200
41
-
42
- # 頁面設定
43
- st.set_page_config(
44
- page_title="KSTools License Manager",
45
- page_icon="🔑",
46
- layout="wide",
47
- initial_sidebar_state="expanded",
48
- menu_items={
49
- 'Get Help': None,
50
- 'Report a bug': None,
51
- 'About': "KSTools License Manager v1.0"
52
- }
53
- )
54
-
55
- # 現代化深色主題 CSS
56
- st.markdown("""
57
- <style>
58
- /* 主要容器和背景 */
59
- .stApp {
60
- background: linear-gradient(135deg, #0d1117 0%, #161b22 50%, #21262d 100%);
61
- color: #e6edf3;
62
- }
63
-
64
- /* 移除 Streamlit 品牌 */
65
- #MainMenu {visibility: hidden;}
66
- footer {visibility: hidden;}
67
- .stDeployButton {display: none;}
68
-
69
- /* 側邊欄現代化 */
70
- .css-1d391kg, .css-1lcbmhc, .css-17eq0hr, section[data-testid="stSidebar"] {
71
- background: linear-gradient(180deg, #161b22 0%, #21262d 100%) !important;
72
- border-right: 1px solid #30363d !important;
73
- }
74
-
75
- .css-1d391kg .css-1v3fvcr, .css-1lcbmhc .css-1v3fvcr {
76
- background: transparent !important;
77
- }
78
-
79
- /* 主內容區域 */
80
- .css-18e3th9, .css-1d391kg, .block-container {
81
- background: transparent !important;
82
- padding-top: 2rem !important;
83
- }
84
-
85
- /* 標題樣式 */
86
- .main-header {
87
- color: #58a6ff;
88
- font-size: 3rem;
89
- font-weight: 800;
90
- text-align: center;
91
- margin-bottom: 2.5rem;
92
- background: linear-gradient(135deg, #58a6ff, #79c0ff);
93
- background-clip: text;
94
- -webkit-background-clip: text;
95
- -webkit-text-fill-color: transparent;
96
- text-shadow: 0 0 30px rgba(88, 166, 255, 0.3);
97
- }
98
-
99
- .sub-header {
100
- color: #7d8590;
101
- font-size: 1.5rem;
102
- font-weight: 600;
103
- margin: 1.5rem 0 1rem 0;
104
- border-bottom: 2px solid #30363d;
105
- padding-bottom: 0.5rem;
106
- }
107
-
108
- /* 現代化卡片設計 */
109
- .modern-card {
110
- background: linear-gradient(135deg, #21262d 0%, #30363d 100%);
111
- padding: 2rem;
112
- border-radius: 16px;
113
- border: 1px solid #30363d;
114
- box-shadow:
115
- 0 4px 6px rgba(0, 0, 0, 0.3),
116
- 0 1px 3px rgba(0, 0, 0, 0.2),
117
- inset 0 1px 0 rgba(255, 255, 255, 0.1);
118
- margin-bottom: 1.5rem;
119
- transition: all 0.3s ease;
120
- backdrop-filter: blur(10px);
121
- }
122
-
123
- .modern-card:hover {
124
- transform: translateY(-2px);
125
- box-shadow:
126
- 0 8px 12px rgba(0, 0, 0, 0.4),
127
- 0 4px 6px rgba(0, 0, 0, 0.3),
128
- inset 0 1px 0 rgba(255, 255, 255, 0.15);
129
- }
130
-
131
- .metric-card {
132
- background: linear-gradient(135deg, #30363d 0%, #424a53 100%);
133
- padding: 1.5rem;
134
- border-radius: 12px;
135
- border: 1px solid #424a53;
136
- text-align: center;
137
- position: relative;
138
- overflow: hidden;
139
- }
140
-
141
- .metric-card::before {
142
- content: '';
143
- position: absolute;
144
- top: 0;
145
- left: 0;
146
- right: 0;
147
- height: 3px;
148
- background: linear-gradient(90deg, #58a6ff, #79c0ff, #a5f3fc);
149
- }
150
-
151
- .metric-card h3 {
152
- color: #7d8590;
153
- font-size: 0.9rem;
154
- font-weight: 600;
155
- margin: 0 0 0.5rem 0;
156
- text-transform: uppercase;
157
- letter-spacing: 0.5px;
158
- }
159
-
160
- .metric-card h2 {
161
- font-size: 2.5rem;
162
- font-weight: 800;
163
- margin: 0;
164
- background: linear-gradient(135deg, #58a6ff, #79c0ff);
165
- background-clip: text;
166
- -webkit-background-clip: text;
167
- -webkit-text-fill-color: transparent;
168
- }
169
-
170
- /* 狀態指示器 */
171
- .status-success {
172
- color: #3fb950;
173
- font-weight: 600;
174
- display: inline-flex;
175
- align-items: center;
176
- gap: 0.5rem;
177
- }
178
-
179
- .status-warning {
180
- color: #d29922;
181
- font-weight: 600;
182
- display: inline-flex;
183
- align-items: center;
184
- gap: 0.5rem;
185
- }
186
-
187
- .status-error {
188
- color: #f85149;
189
- font-weight: 600;
190
- display: inline-flex;
191
- align-items: center;
192
- gap: 0.5rem;
193
- }
194
-
195
- .status-info {
196
- color: #58a6ff;
197
- font-weight: 600;
198
- display: inline-flex;
199
- align-items: center;
200
- gap: 0.5rem;
201
- }
202
-
203
- /* 按鈕現代化 */
204
- .stButton > button {
205
- background: linear-gradient(135deg, #238636 0%, #2ea043 100%);
206
- color: white;
207
- border: none;
208
- border-radius: 8px;
209
- padding: 0.75rem 1.5rem;
210
- font-weight: 600;
211
- font-size: 0.9rem;
212
- transition: all 0.3s ease;
213
- box-shadow: 0 2px 4px rgba(35, 134, 54, 0.3);
214
- }
215
-
216
- .stButton > button:hover {
217
- background: linear-gradient(135deg, #2ea043 0%, #46954a 100%);
218
- transform: translateY(-1px);
219
- box-shadow: 0 4px 8px rgba(35, 134, 54, 0.4);
220
- }
221
-
222
- .stButton > button:active {
223
- transform: translateY(0);
224
- box-shadow: 0 1px 2px rgba(35, 134, 54, 0.3);
225
- }
226
-
227
- /* 危險按鈕 */
228
- .stButton > button[data-kind="secondary"] {
229
- background: linear-gradient(135deg, #da3633 0%, #f85149 100%);
230
- box-shadow: 0 2px 4px rgba(218, 54, 51, 0.3);
231
- }
232
-
233
- .stButton > button[data-kind="secondary"]:hover {
234
- background: linear-gradient(135deg, #f85149 0%, #ff6b6b 100%);
235
- box-shadow: 0 4px 8px rgba(218, 54, 51, 0.4);
236
- }
237
-
238
- /* 表單元素 */
239
- .stTextInput > div > div > input,
240
- .stTextArea > div > div > textarea,
241
- .stSelectbox > div > div > select {
242
- background: #21262d !important;
243
- border: 1px solid #30363d !important;
244
- border-radius: 8px !important;
245
- color: #e6edf3 !important;
246
- padding: 0.75rem !important;
247
- }
248
-
249
- .stTextInput > div > div > input:focus,
250
- .stTextArea > div > div > textarea:focus,
251
- .stSelectbox > div > div > select:focus {
252
- border-color: #58a6ff !important;
253
- box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.3) !important;
254
- }
255
-
256
- /* 資料表格現代化 */
257
- .dataframe {
258
- background: #21262d;
259
- border: 1px solid #30363d;
260
- border-radius: 12px;
261
- overflow: hidden;
262
- }
263
-
264
- .dataframe thead tr {
265
- background: #30363d !important;
266
- }
267
-
268
- .dataframe tbody tr:hover {
269
- background: rgba(88, 166, 255, 0.1) !important;
270
- }
271
-
272
- /* 載入動畫 */
273
- .loading-spinner {
274
- display: inline-block;
275
- width: 20px;
276
- height: 20px;
277
- border: 2px solid #30363d;
278
- border-radius: 50%;
279
- border-top-color: #58a6ff;
280
- animation: spin 1s ease-in-out infinite;
281
- margin-right: 0.5rem;
282
- }
283
-
284
- @keyframes spin {
285
- to { transform: rotate(360deg); }
286
- }
287
-
288
- /* 進度條 */
289
- .stProgress > div > div > div > div {
290
- background: linear-gradient(90deg, #58a6ff, #79c0ff) !important;
291
- }
292
-
293
- /* 通知樣式 */
294
- .stAlert {
295
- border-radius: 8px !important;
296
- border: none !important;
297
- backdrop-filter: blur(10px) !important;
298
- }
299
-
300
- .stAlert[data-baseweb="notification"] {
301
- background: rgba(33, 38, 45, 0.9) !important;
302
- }
303
-
304
- /* 響應式設計 */
305
- @media (max-width: 768px) {
306
- .main-header {
307
- font-size: 2rem;
308
- }
309
-
310
- .modern-card {
311
- padding: 1rem;
312
- margin-bottom: 1rem;
313
- }
314
-
315
- .metric-card h2 {
316
- font-size: 1.8rem;
317
- }
318
- }
319
-
320
- /* 自定義滾動條 */
321
- ::-webkit-scrollbar {
322
- width: 8px;
323
- height: 8px;
324
- }
325
-
326
- ::-webkit-scrollbar-track {
327
- background: #21262d;
328
- }
329
-
330
- ::-webkit-scrollbar-thumb {
331
- background: #424a53;
332
- border-radius: 4px;
333
- }
334
-
335
- ::-webkit-scrollbar-thumb:hover {
336
- background: #6e7681;
337
- }
338
- </style>
339
- """, unsafe_allow_html=True)
340
-
341
- # 工具函數類
342
- class ApiClient:
343
- """優化的 API 客戶端"""
344
-
345
- @staticmethod
346
- def _make_request(method: str, endpoint: str, data: Optional[Dict] = None, retries: int = 3) -> ApiResponse:
347
- """統一的 API 請求處理"""
348
- for attempt in range(retries):
349
- try:
350
- url = f"{Config.API_BASE}{endpoint}"
351
-
352
- if method.upper() == "GET":
353
- response = requests.get(url, timeout=Config.REQUEST_TIMEOUT)
354
- elif method.upper() == "POST":
355
- response = requests.post(url, json=data, timeout=Config.REQUEST_TIMEOUT)
356
- elif method.upper() == "DELETE":
357
- response = requests.delete(url, timeout=Config.REQUEST_TIMEOUT)
358
- elif method.upper() == "PATCH":
359
- response = requests.patch(url, json=data, timeout=Config.REQUEST_TIMEOUT)
360
- else:
361
- return ApiResponse(False, message="不支援的 HTTP 方法")
362
-
363
- if response.status_code == 200:
364
- result = response.json()
365
- return ApiResponse(
366
- success=result.get("success", True),
367
- data=result.get("data"),
368
- message=result.get("message", ""),
369
- status_code=response.status_code
370
- )
371
- else:
372
- return ApiResponse(
373
- False,
374
- message=f"HTTP {response.status_code}: {response.text}",
375
- status_code=response.status_code
376
- )
377
-
378
- except requests.exceptions.Timeout:
379
- if attempt == retries - 1:
380
- return ApiResponse(False, message="請求超時,請檢查網路連接")
381
- time.sleep(1 * (attempt + 1)) # 指數退避
382
-
383
- except requests.exceptions.ConnectionError:
384
- if attempt == retries - 1:
385
- return ApiResponse(False, message="無法連接到 API 服務,請檢查服務狀態")
386
- time.sleep(1 * (attempt + 1))
387
-
388
- except Exception as e:
389
- return ApiResponse(False, message=f"未預期的錯誤: {str(e)}")
390
-
391
- return ApiResponse(False, message="請求失敗")
392
-
393
- @staticmethod
394
- @st.cache_data(ttl=Config.CACHE_TTL)
395
- def get(endpoint: str) -> ApiResponse:
396
- """GET 請求"""
397
- return ApiClient._make_request("GET", endpoint)
398
-
399
- @staticmethod
400
- def post(endpoint: str, data: Dict) -> ApiResponse:
401
- """POST 請求"""
402
- return ApiClient._make_request("POST", endpoint, data)
403
-
404
- @staticmethod
405
- def delete(endpoint: str) -> ApiResponse:
406
- """DELETE 請求"""
407
- return ApiClient._make_request("DELETE", endpoint)
408
-
409
- @staticmethod
410
- def patch(endpoint: str, data: Optional[Dict] = None) -> ApiResponse:
411
- """PATCH 請求"""
412
- return ApiClient._make_request("PATCH", endpoint, data)
413
-
414
- class UIComponents:
415
- """UI 組件庫"""
416
-
417
- @staticmethod
418
- def show_status_message(status: Status, message: str, details: str = ""):
419
- """顯示狀態訊息"""
420
- if status == Status.SUCCESS:
421
- st.success(f"✅ {message}")
422
- elif status == Status.ERROR:
423
- st.error(f"❌ {message}")
424
- if details:
425
- with st.expander("錯誤詳情"):
426
- st.code(details)
427
- elif status == Status.WARNING:
428
- st.warning(f"⚠️ {message}")
429
- else:
430
- st.info(f"ℹ️ {message}")
431
-
432
- @staticmethod
433
- def create_metric_card(title: str, value: str, color: str = "#58a6ff") -> str:
434
- """建立指標卡片"""
435
- return f"""
436
- <div class="metric-card">
437
- <h3>{title}</h3>
438
- <h2 style="color: {color};">{value}</h2>
439
- </div>
440
- """
441
-
442
- @staticmethod
443
- def show_loading(message: str = "載入中..."):
444
- """顯示載入狀態"""
445
- return st.markdown(f'<div class="loading-spinner"></div>{message}', unsafe_allow_html=True)
446
-
447
- @staticmethod
448
- def format_datetime(dt_string: str) -> str:
449
- """格式化日期時間"""
450
- try:
451
- dt = datetime.fromisoformat(dt_string.replace('Z', '+00:00'))
452
- return dt.strftime("%Y-%m-%d %H:%M")
453
- except:
454
- return "無效日期"
455
-
456
- @staticmethod
457
- def format_license_status(license_data: Dict) -> str:
458
- """格式化授權狀態"""
459
- try:
460
- expires_at = datetime.fromisoformat(license_data["expires_at"].replace('Z', '+00:00'))
461
- now = datetime.now(timezone.utc)
462
-
463
- if expires_at < now:
464
- return '<span class="status-error">❌ 已過期</span>'
465
- elif license_data.get("is_active", False):
466
- return '<span class="status-success">✅ 啟用中</span>'
467
- else:
468
- return '<span class="status-warning">⏸️ 已停用</span>'
469
- except:
470
- return '<span class="status-error">❓ 狀態未知</span>'
471
-
472
- # 頁面組件
473
- class DashboardPage:
474
- """儀表板頁面"""
475
-
476
- @staticmethod
477
- def show():
478
- st.markdown('<h1 class="main-header">📊 系統儀表板</h1>', unsafe_allow_html=True)
479
-
480
- # 載入統計資料
481
- with st.spinner("載入統計資料中..."):
482
- stats_response = ApiClient.get("/licenses/stats")
483
-
484
- if not stats_response.success:
485
- UIComponents.show_status_message(
486
- Status.ERROR,
487
- "無法載入統計資料",
488
- stats_response.message
489
- )
490
- return
491
-
492
- stats = stats_response.data or {}
493
-
494
- # 統計卡片 - 4列佈局
495
- cols = st.columns(4)
496
-
497
- metrics = [
498
- ("🔑 總授權數", stats.get("total_licenses", 0), "#58a6ff"),
499
- ("✅ 啟用中", stats.get("active_licenses", 0), "#3fb950"),
500
- ("⏰ 已過期", stats.get("expired_licenses", 0), "#f85149"),
501
- ("📈 今日啟用", stats.get("today_activations", 0), "#d29922")
502
- ]
503
-
504
- for i, (title, value, color) in enumerate(metrics):
505
- with cols[i]:
506
- st.markdown(
507
- UIComponents.create_metric_card(title, str(value), color),
508
- unsafe_allow_html=True
509
- )
510
-
511
- # 圖表區域
512
- st.markdown("---")
513
- chart_cols = st.columns(2)
514
-
515
- with chart_cols[0]:
516
- DashboardPage._show_status_pie_chart(stats)
517
-
518
- with chart_cols[1]:
519
- DashboardPage._show_activation_trend(stats)
520
-
521
- # 最近活動
522
- st.markdown("---")
523
- DashboardPage._show_recent_activities()
524
-
525
- @staticmethod
526
- def _show_status_pie_chart(stats: Dict):
527
- """顯示狀態分布圓餅圖"""
528
- st.markdown('<h3 class="sub-header">🔄 授權狀態分布</h3>', unsafe_allow_html=True)
529
-
530
- data = {
531
- "啟用中": stats.get("active_licenses", 0),
532
- "已停用": stats.get("inactive_licenses", 0),
533
- "已過期": stats.get("expired_licenses", 0)
534
- }
535
-
536
- if sum(data.values()) == 0:
537
- st.info("暫無授權資料")
538
- return
539
-
540
- fig = px.pie(
541
- values=list(data.values()),
542
- names=list(data.keys()),
543
- color_discrete_sequence=["#3fb950", "#d29922", "#f85149"],
544
- title="授權狀態分布"
545
- )
546
-
547
- fig.update_traces(
548
- textposition='inside',
549
- textinfo='percent+label',
550
- textfont_size=12
551
- )
552
-
553
- fig.update_layout(
554
- plot_bgcolor='rgba(0,0,0,0)',
555
- paper_bgcolor='rgba(0,0,0,0)',
556
- font_color='#e6edf3',
557
- title_font_size=16,
558
- title_x=0.5,
559
- showlegend=True,
560
- legend=dict(
561
- orientation="h",
562
- yanchor="bottom",
563
- y=1.02,
564
- xanchor="right",
565
- x=1
566
- )
567
- )
568
-
569
- st.plotly_chart(fig, use_container_width=True)
570
-
571
- @staticmethod
572
- def _show_activation_trend(stats: Dict):
573
- """顯示啟用趨勢"""
574
- st.markdown('<h3 class="sub-header">📊 啟用趨勢</h3>', unsafe_allow_html=True)
575
-
576
- trend_data = {
577
- "期間": ["今日", "本週", "本月"],
578
- "啟用數": [
579
- stats.get("today_activations", 0),
580
- stats.get("this_week_activations", 0),
581
- stats.get("this_month_activations", 0)
582
- ]
583
- }
584
-
585
- fig = px.bar(
586
- x=trend_data["期間"],
587
- y=trend_data["啟用數"],
588
- color=trend_data["啟用數"],
589
- color_continuous_scale=["#21262d", "#58a6ff"],
590
- title="授權啟用趨勢",
591
- text=trend_data["啟用數"]
592
- )
593
-
594
- fig.update_traces(texttemplate='%{text}', textposition='outside')
595
- fig.update_layout(
596
- plot_bgcolor='rgba(0,0,0,0)',
597
- paper_bgcolor='rgba(0,0,0,0)',
598
- font_color='#e6edf3',
599
- title_font_size=16,
600
- title_x=0.5,
601
- showlegend=False,
602
- yaxis_title="啟用次數",
603
- xaxis_title="時間段"
604
- )
605
-
606
- st.plotly_chart(fig, use_container_width=True)
607
-
608
- @staticmethod
609
- def _show_recent_activities():
610
- """顯示最近活動"""
611
- st.markdown('<h3 class="sub-header">🕒 最近活動</h3>', unsafe_allow_html=True)
612
-
613
- with st.spinner("載入活動記錄..."):
614
- logs_response = ApiClient.get("/licenses/logs?limit=10")
615
-
616
- if not logs_response.success:
617
- st.error("無法載入活動記錄")
618
- return
619
-
620
- logs = logs_response.data or []
621
-
622
- if not logs:
623
- st.info("暫無活動記錄")
624
- return
625
-
626
- # 建立活動表格
627
- activities = []
628
- for log in logs:
629
- activities.append({
630
- "時間": UIComponents.format_datetime(log.get("created_at", "")),
631
- "動作": log.get("action", "未知"),
632
- "用戶": log.get("licenses", {}).get("user_name", "未知") if log.get("licenses") else "未知",
633
- "狀態": "成功" if not log.get("error_message") else "失敗",
634
- "IP": log.get("ip_address", "未知")
635
- })
636
-
637
- df = pd.DataFrame(activities)
638
-
639
- # 自定義樣式
640
- def style_status(val):
641
- color = "#3fb950" if val == "成功" else "#f85149"
642
- return f"background-color: {color}; color: white; padding: 4px 8px; border-radius: 4px; text-align: center;"
643
-
644
- styled_df = df.style.applymap(style_status, subset=["狀態"])
645
- st.dataframe(styled_df, use_container_width=True, height=300)
646
-
647
- class UserManagementPage:
648
- """用戶管理頁面"""
649
-
650
- @staticmethod
651
- def show():
652
- st.markdown('<h1 class="main-header">👥 用戶管理</h1>', unsafe_allow_html=True)
653
-
654
- # 新增授權區域
655
- UserManagementPage._show_create_license_form()
656
-
657
- st.markdown("---")
658
-
659
- # 授權列表
660
- UserManagementPage._show_license_list()
661
-
662
- @staticmethod
663
- def _show_create_license_form():
664
- """新增授權表單"""
665
- with st.expander("➕ 建立新授權", expanded=False):
666
- # 防止重複提交的關鍵
667
- form_key = f"create_license_form_{int(time.time())}"
668
- with st.form(form_key, clear_on_submit=True):
669
- st.markdown("### 基本資訊")
670
-
671
- col1, col2 = st.columns(2)
672
-
673
- with col1:
674
- user_name = st.text_input(
675
- "用戶名稱 *",
676
- help="輸入授權用戶的名稱",
677
- placeholder="請輸入用戶名稱..."
678
- )
679
- user_email = st.text_input(
680
- "電子郵件",
681
- help="選填,用於通知和聯絡",
682
- placeholder="user@example.com"
683
- )
684
-
685
- with col2:
686
- expires_options = [
687
- (7, "7 天 (測試)"),
688
- (30, "30 天 (標準)"),
689
- (90, "90 天 (季度)"),
690
- (365, "365 天 (年度)"),
691
- (730, "730 天 (兩年)"),
692
- (1095, "1095 天 (三年)")
693
- ]
694
-
695
- expires_days = st.selectbox(
696
- "授權期限",
697
- options=[days for days, _ in expires_options],
698
- format_func=lambda x: next(label for days, label in expires_options if days == x),
699
- index=1 # 預設30天
700
- )
701
-
702
- hardware_id = st.text_input(
703
- "硬體ID",
704
- help="選填,可稍後由用戶啟用時綁定",
705
- placeholder="自動生成或手動輸入..."
706
- )
707
-
708
- st.markdown("### 授權設定")
709
-
710
- col3, col4 = st.columns(2)
711
- with col3:
712
- auto_activate = st.checkbox("自動啟用", value=True, help="建立後立即啟用授權")
713
-
714
- with col4:
715
- send_email = st.checkbox("發送通知", value=False, help="建立後發送郵件通知(需要郵件設定)")
716
-
717
- submitted = st.form_submit_button("🔑 建立授權", use_container_width=True, type="primary")
718
-
719
- if submitted:
720
- # 防止重複提交
721
- if st.session_state.get("creating_license", False):
722
- st.warning("⏳ 正在建立授權中,請稍候...")
723
- return
724
-
725
- if not user_name.strip():
726
- st.error("請輸入用戶名稱")
727
- return
728
-
729
- # 設定正在處理狀態
730
- st.session_state.creating_license = True
731
-
732
- license_data = {
733
- "user_name": user_name.strip(),
734
- "user_email": user_email.strip() if user_email.strip() else None,
735
- "expires_days": expires_days,
736
- "hardware_id": hardware_id.strip() if hardware_id.strip() else None,
737
- "is_active": auto_activate
738
- }
739
-
740
- try:
741
- with st.spinner("建立授權中..."):
742
- result = ApiClient.post("/license/create", license_data)
743
- finally:
744
- # 完成後重置狀態
745
- st.session_state.creating_license = False
746
-
747
- if result.success:
748
- # 設定成功狀態,防止重複提交
749
- st.session_state.license_created = True
750
- st.session_state.new_license_info = result.data
751
-
752
- st.success("✅ 授權建立成功!")
753
-
754
- # 顯示授權資訊
755
- st.markdown("### 📋 授權資訊")
756
- license_info = result.data
757
-
758
- info_col1, info_col2 = st.columns(2)
759
- with info_col1:
760
- st.code(f"授權碼: {license_info.get('license_code', 'N/A')}")
761
- with info_col2:
762
- st.code(f"到期時間: {license_info.get('expires_at', 'N/A')}")
763
-
764
- # 清除快取但不重新運行
765
- st.cache_data.clear()
766
-
767
- # 顯示重新整理提示
768
- if st.button("🔄 重新整理列表", type="secondary"):
769
- st.rerun()
770
- else:
771
- UIComponents.show_status_message(
772
- Status.ERROR,
773
- "建立失敗",
774
- result.message
775
- )
776
-
777
- @staticmethod
778
- def _show_license_list():
779
- """顯示授權列表"""
780
- st.markdown('<h3 class="sub-header">📋 授權列表</h3>', unsafe_allow_html=True)
781
-
782
- # 搜尋和篩選控制
783
- col1, col2, col3, col4 = st.columns([3, 1, 1, 1])
784
-
785
- with col1:
786
- search_term = st.text_input(
787
- "🔍 搜尋",
788
- placeholder="用戶名稱、郵箱、授權碼...",
789
- key="search_licenses"
790
- )
791
-
792
- with col2:
793
- status_filter = st.selectbox(
794
- "狀態篩選",
795
- ["全部", "啟用中", "已停用", "已過期", "未啟用"],
796
- key="status_filter"
797
- )
798
-
799
- with col3:
800
- page_size = st.selectbox(
801
- "每頁顯示",
802
- [10, 20, 50, 100],
803
- index=1,
804
- key="page_size"
805
- )
806
-
807
- with col4:
808
- if st.button("🔄 重新整理", use_container_width=True):
809
- st.cache_data.clear()
810
- st.rerun()
811
-
812
- # 載入授權列表
813
- with st.spinner("載入授權列表..."):
814
- licenses_response = ApiClient.get("/licenses")
815
-
816
- if not licenses_response.success:
817
- UIComponents.show_status_message(
818
- Status.ERROR,
819
- "無法載入授權列表",
820
- licenses_response.message
821
- )
822
- return
823
-
824
- licenses = licenses_response.data or []
825
-
826
- if not licenses:
827
- st.info("📋 目前沒有任何授權記錄")
828
- return
829
-
830
- # 篩選和分頁
831
- filtered_licenses = UserManagementPage._filter_licenses(licenses, search_term, status_filter)
832
-
833
- if not filtered_licenses:
834
- st.info("🔍 沒有符合條件的授權")
835
- return
836
-
837
- # 分頁邏輯
838
- total_pages = (len(filtered_licenses) + page_size - 1) // page_size
839
-
840
- if total_pages > 1:
841
- col1, col2, col3 = st.columns([1, 2, 1])
842
- with col2:
843
- current_page = st.select_slider(
844
- "頁數",
845
- options=list(range(1, total_pages + 1)),
846
- value=1,
847
- key="current_page"
848
- )
849
- else:
850
- current_page = 1
851
-
852
- # 顯示當前頁面的授權
853
- start_idx = (current_page - 1) * page_size
854
- end_idx = min(start_idx + page_size, len(filtered_licenses))
855
- current_licenses = filtered_licenses[start_idx:end_idx]
856
-
857
- # 授權卡片顯示
858
- for license_data in current_licenses:
859
- UserManagementPage._show_license_card(license_data)
860
-
861
- # 顯示統計資訊
862
- st.markdown("---")
863
- st.info(f"顯示 {start_idx + 1}-{end_idx} / {len(filtered_licenses)} 筆授權 (共 {len(licenses)} 筆)")
864
-
865
- @staticmethod
866
- def _filter_licenses(licenses: List[Dict], search_term: str, status_filter: str) -> List[Dict]:
867
- """篩選授權列表"""
868
- filtered = []
869
- now = datetime.now(timezone.utc)
870
-
871
- for license_data in licenses:
872
- # 搜尋過濾
873
- if search_term:
874
- search_fields = [
875
- license_data.get("user_name", "").lower(),
876
- license_data.get("user_email", "").lower(),
877
- license_data.get("license_code", "").lower()
878
- ]
879
- if not any(search_term.lower() in field for field in search_fields):
880
- continue
881
-
882
- # 狀態過濾
883
- expires_at = datetime.fromisoformat(license_data["expires_at"].replace('Z', '+00:00'))
884
- is_expired = expires_at < now
885
- is_activated = license_data.get("activated_at") is not None
886
-
887
- if status_filter == "啟用中" and (not license_data.get("is_active") or is_expired):
888
- continue
889
- elif status_filter == "已停用" and (license_data.get("is_active") or is_expired):
890
- continue
891
- elif status_filter == "已過期" and not is_expired:
892
- continue
893
- elif status_filter == "未啟用" and is_activated:
894
- continue
895
-
896
- filtered.append(license_data)
897
-
898
- return filtered
899
-
900
- @staticmethod
901
- def _show_license_card(license_data: Dict):
902
- """顯示單個授權卡片"""
903
- with st.container():
904
- st.markdown('<div class="modern-card">', unsafe_allow_html=True)
905
-
906
- col1, col2, col3, col4 = st.columns([3, 2, 2, 2])
907
-
908
- with col1:
909
- st.markdown(f"**👤 {license_data.get('user_name', '未知用戶')}**")
910
- if license_data.get("user_email"):
911
- st.markdown(f"📧 {license_data['user_email']}")
912
- st.code(f"🔑 {license_data.get('license_code', 'N/A')}")
913
-
914
- with col2:
915
- # 狀態顯示
916
- status_html = UIComponents.format_license_status(license_data)
917
- st.markdown(status_html, unsafe_allow_html=True)
918
-
919
- # 到期時間
920
- expires_at = UIComponents.format_datetime(license_data.get("expires_at", ""))
921
- st.markdown(f"⏰ {expires_at}")
922
-
923
- with col3:
924
- # 硬體資訊
925
- if license_data.get("hardware_id"):
926
- st.markdown(f"💻 {license_data['hardware_id'][:12]}...")
927
- else:
928
- st.markdown("💻 未綁定")
929
-
930
- # 最後使用
931
- if license_data.get("last_used_at"):
932
- last_used = UIComponents.format_datetime(license_data["last_used_at"])
933
- st.markdown(f"🕒 {last_used}")
934
- else:
935
- st.markdown("🕒 未使用")
936
-
937
- with col4:
938
- # 操作按鈕
939
- btn_col1, btn_col2, btn_col3 = st.columns(3)
940
-
941
- with btn_col1:
942
- # 切換狀態
943
- is_active = license_data.get("is_active", False)
944
- if st.button(
945
- "⏸️" if is_active else "▶️",
946
- key=f"toggle_{license_data.get('id')}",
947
- help="切換授權狀態",
948
- use_container_width=True
949
- ):
950
- UserManagementPage._toggle_license_status(license_data.get("id"))
951
-
952
- with btn_col2:
953
- # 編輯按鈕
954
- if st.button(
955
- "✏️",
956
- key=f"edit_{license_data.get('id')}",
957
- help="編輯授權",
958
- use_container_width=True
959
- ):
960
- st.session_state[f"edit_mode_{license_data.get('id')}"] = True
961
- st.rerun()
962
-
963
- with btn_col3:
964
- # 刪除按鈕
965
- if st.button(
966
- "🗑️",
967
- key=f"delete_{license_data.get('id')}",
968
- help="刪除授權",
969
- use_container_width=True
970
- ):
971
- UserManagementPage._delete_license(license_data.get("id"))
972
-
973
- st.markdown('</div>', unsafe_allow_html=True)
974
- st.markdown("---")
975
-
976
- @staticmethod
977
- def _toggle_license_status(license_id: str):
978
- """切換授權狀態"""
979
- with st.spinner("更新狀態中..."):
980
- result = ApiClient.patch(f"/license/{license_id}/toggle")
981
-
982
- if result.success:
983
- st.success("授權狀態已更新")
984
- st.cache_data.clear()
985
- # 設定延遲重新載入,避免重複操作
986
- st.session_state.refresh_needed = True
987
- time.sleep(1)
988
- if st.session_state.get("refresh_needed"):
989
- st.rerun()
990
- else:
991
- UIComponents.show_status_message(Status.ERROR, "操作失敗", result.message)
992
-
993
- @staticmethod
994
- def _delete_license(license_id: str):
995
- """刪除授權"""
996
- confirm_key = f"confirm_delete_{license_id}"
997
-
998
- if not st.session_state.get(confirm_key, False):
999
- st.session_state[confirm_key] = True
1000
- st.warning("再次點擊確認刪除")
1001
- return
1002
-
1003
- with st.spinner("刪除中..."):
1004
- result = ApiClient.delete(f"/license/{license_id}")
1005
-
1006
- if result.success:
1007
- st.success("授權已刪除")
1008
- st.cache_data.clear()
1009
- if confirm_key in st.session_state:
1010
- del st.session_state[confirm_key]
1011
- # 避免立即重新載入導致重複操作
1012
- st.info("頁面將在 3 秒後自動重新整理...")
1013
- time.sleep(3)
1014
- st.rerun()
1015
- else:
1016
- UIComponents.show_status_message(Status.ERROR, "刪除失敗", result.message)
1017
-
1018
- # 其他頁面類別繼續...
1019
- class LicenseLogsPage:
1020
- """授權記錄頁面"""
1021
-
1022
- @staticmethod
1023
- def show():
1024
- st.markdown('<h1 class="main-header">📜 授權記錄</h1>', unsafe_allow_html=True)
1025
-
1026
- # 控制選項
1027
- col1, col2, col3, col4 = st.columns(4)
1028
-
1029
- with col1:
1030
- log_limit = st.selectbox("顯示筆數", [50, 100, 200, 500], index=1)
1031
-
1032
- with col2:
1033
- action_filter = st.selectbox(
1034
- "動作篩選",
1035
- ["全部", "activate", "validate", "error"]
1036
- )
1037
-
1038
- with col3:
1039
- date_range = st.selectbox(
1040
- "時間範圍",
1041
- ["全部", "今日", "本週", "本月"]
1042
- )
1043
-
1044
- with col4:
1045
- if st.button("🔄 重新整理", use_container_width=True):
1046
- st.cache_data.clear()
1047
- st.rerun()
1048
-
1049
- # 載入使用記錄
1050
- with st.spinner("載入使用記錄..."):
1051
- logs_response = ApiClient.get(f"/licenses/logs?limit={log_limit}")
1052
-
1053
- if not logs_response.success:
1054
- UIComponents.show_status_message(
1055
- Status.ERROR,
1056
- "無法載入使用記錄",
1057
- logs_response.message
1058
- )
1059
- return
1060
-
1061
- logs = logs_response.data or []
1062
-
1063
- if not logs:
1064
- st.info("📜 目前沒有任何使用記錄")
1065
- return
1066
-
1067
- # 篩選記錄
1068
- filtered_logs = LicenseLogsPage._filter_logs(logs, action_filter, date_range)
1069
-
1070
- if not filtered_logs:
1071
- st.info("🔍 沒有符合條件的記錄")
1072
- return
1073
-
1074
- # 顯示統計資訊
1075
- LicenseLogsPage._show_log_statistics(filtered_logs)
1076
-
1077
- st.markdown("---")
1078
-
1079
- # 顯示詳細記錄
1080
- LicenseLogsPage._show_log_details(filtered_logs)
1081
-
1082
- st.markdown("---")
1083
-
1084
- # 顯示分析圖表
1085
- LicenseLogsPage._show_log_charts(filtered_logs)
1086
-
1087
- @staticmethod
1088
- def _filter_logs(logs: List[Dict], action_filter: str, date_range: str) -> List[Dict]:
1089
- """篩選記錄"""
1090
- filtered = logs.copy()
1091
-
1092
- # 動作篩選
1093
- if action_filter != "全部":
1094
- filtered = [log for log in filtered if log.get("action") == action_filter]
1095
-
1096
- # 時間篩選
1097
- if date_range != "全部":
1098
- now = datetime.now(timezone.utc)
1099
-
1100
- if date_range == "今日":
1101
- start_time = now.replace(hour=0, minute=0, second=0, microsecond=0)
1102
- elif date_range == "本週":
1103
- start_time = now - timedelta(days=now.weekday())
1104
- start_time = start_time.replace(hour=0, minute=0, second=0, microsecond=0)
1105
- elif date_range == "本月":
1106
- start_time = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
1107
- else:
1108
- start_time = None
1109
-
1110
- if start_time:
1111
- filtered = [
1112
- log for log in filtered
1113
- if datetime.fromisoformat(log.get("created_at", "").replace('Z', '+00:00')) >= start_time
1114
- ]
1115
-
1116
- return filtered
1117
-
1118
- @staticmethod
1119
- def _show_log_statistics(logs: List[Dict]):
1120
- """顯示記錄統計"""
1121
- col1, col2, col3, col4 = st.columns(4)
1122
-
1123
- with col1:
1124
- activate_count = len([l for l in logs if l.get("action") == "activate"])
1125
- st.metric("啟用次數", activate_count)
1126
-
1127
- with col2:
1128
- validate_count = len([l for l in logs if l.get("action") == "validate"])
1129
- st.metric("驗證次數", validate_count)
1130
-
1131
- with col3:
1132
- error_count = len([l for l in logs if l.get("error_message")])
1133
- st.metric("錯誤次數", error_count)
1134
-
1135
- with col4:
1136
- unique_ips = len(set([l.get("ip_address") for l in logs if l.get("ip_address")]))
1137
- st.metric("不同 IP", unique_ips)
1138
-
1139
- @staticmethod
1140
- def _show_log_details(logs: List[Dict]):
1141
- """顯示記錄詳情"""
1142
- st.markdown('<h3 class="sub-header">📊 詳細記錄</h3>', unsafe_allow_html=True)
1143
-
1144
- # 建立記錄表格
1145
- log_data = []
1146
- for log in logs:
1147
- log_data.append({
1148
- "時間": UIComponents.format_datetime(log.get("created_at", "")),
1149
- "動作": log.get("action", "未知"),
1150
- "用戶": log.get("licenses", {}).get("user_name", "未知") if log.get("licenses") else "未知",
1151
- "授權碼": log.get("licenses", {}).get("license_code", "未知") if log.get("licenses") else "未知",
1152
- "IP地址": log.get("ip_address", "未知"),
1153
- "狀態": "失敗" if log.get("error_message") else "成功",
1154
- "錯誤訊息": log.get("error_message", "")
1155
- })
1156
-
1157
- df = pd.DataFrame(log_data)
1158
-
1159
- # 樣式設定
1160
- def highlight_status(val):
1161
- if val == "成功":
1162
- return "background-color: #3fb950; color: white; padding: 4px 8px; border-radius: 4px;"
1163
- elif val == "失敗":
1164
- return "background-color: #f85149; color: white; padding: 4px 8px; border-radius: 4px;"
1165
- return ""
1166
-
1167
- styled_df = df.style.applymap(highlight_status, subset=["狀態"])
1168
- st.dataframe(styled_df, use_container_width=True, height=400)
1169
-
1170
- @staticmethod
1171
- def _show_log_charts(logs: List[Dict]):
1172
- """顯示分析圖表"""
1173
- st.markdown('<h3 class="sub-header">📈 使用分析</h3>', unsafe_allow_html=True)
1174
-
1175
- col1, col2 = st.columns(2)
1176
-
1177
- with col1:
1178
- # 動作分布圓餅圖
1179
- action_counts = {}
1180
- for log in logs:
1181
- action = log.get("action", "未知")
1182
- action_counts[action] = action_counts.get(action, 0) + 1
1183
-
1184
- if action_counts:
1185
- fig = px.pie(
1186
- values=list(action_counts.values()),
1187
- names=list(action_counts.keys()),
1188
- title="動作分布",
1189
- color_discrete_sequence=px.colors.qualitative.Set3
1190
- )
1191
-
1192
- fig.update_layout(
1193
- plot_bgcolor='rgba(0,0,0,0)',
1194
- paper_bgcolor='rgba(0,0,0,0)',
1195
- font_color='#e6edf3',
1196
- title_x=0.5
1197
- )
1198
-
1199
- st.plotly_chart(fig, use_container_width=True)
1200
-
1201
- with col2:
1202
- # 24小時使用分布
1203
- hourly_counts = {}
1204
- for log in logs:
1205
- try:
1206
- dt = datetime.fromisoformat(log.get("created_at", "").replace('Z', '+00:00'))
1207
- hour = dt.hour
1208
- hourly_counts[hour] = hourly_counts.get(hour, 0) + 1
1209
- except:
1210
- continue
1211
-
1212
- if hourly_counts:
1213
- hours = list(range(24))
1214
- counts = [hourly_counts.get(h, 0) for h in hours]
1215
-
1216
- fig = px.bar(
1217
- x=hours,
1218
- y=counts,
1219
- title="24小時使用分布",
1220
- labels={"x": "小時", "y": "使用次數"},
1221
- color=counts,
1222
- color_continuous_scale="blues"
1223
- )
1224
-
1225
- fig.update_layout(
1226
- plot_bgcolor='rgba(0,0,0,0)',
1227
- paper_bgcolor='rgba(0,0,0,0)',
1228
- font_color='#e6edf3',
1229
- title_x=0.5,
1230
- showlegend=False
1231
- )
1232
-
1233
- st.plotly_chart(fig, use_container_width=True)
1234
-
1235
- class SystemSettingsPage:
1236
- """系統設定頁面"""
1237
-
1238
- @staticmethod
1239
- def show():
1240
- st.markdown('<h1 class="main-header">⚙️ 系統設定</h1>', unsafe_allow_html=True)
1241
-
1242
- # 系統狀態檢查
1243
- SystemSettingsPage._show_system_status()
1244
-
1245
- st.markdown("---")
1246
-
1247
- # 系統資訊
1248
- SystemSettingsPage._show_system_info()
1249
-
1250
- st.markdown("---")
1251
-
1252
- # 系統工具
1253
- SystemSettingsPage._show_system_tools()
1254
-
1255
- st.markdown("---")
1256
-
1257
- # 環境設定檢查
1258
- SystemSettingsPage._show_environment_check()
1259
-
1260
- @staticmethod
1261
- def _show_system_status():
1262
- """顯示系統狀態"""
1263
- st.markdown('<h3 class="sub-header">🔧 系統狀態</h3>', unsafe_allow_html=True)
1264
-
1265
- col1, col2 = st.columns(2)
1266
-
1267
- with col1:
1268
- st.markdown("**🌐 API 服務狀態**")
1269
-
1270
- try:
1271
- with st.spinner("檢查 API 服務..."):
1272
- health_response = ApiClient.get("/health")
1273
-
1274
- if health_response.success:
1275
- st.success("✅ API 服務正常運行")
1276
- if health_response.data:
1277
- st.json(health_response.data)
1278
- else:
1279
- st.error(f"❌ API 服務異常: {health_response.message}")
1280
-
1281
- except Exception as e:
1282
- st.error(f"❌ API 服務檢查失敗: {str(e)}")
1283
-
1284
- with col2:
1285
- st.markdown("**🗄️ 資料庫連接**")
1286
-
1287
- try:
1288
- with st.spinner("檢查資料庫連接..."):
1289
- db_test = ApiClient.get("/licenses/stats")
1290
-
1291
- if db_test.success:
1292
- st.success("✅ 資料庫連接正常")
1293
- stats = db_test.data
1294
- if stats:
1295
- st.metric("總授權數", stats.get("total_licenses", 0))
1296
- else:
1297
- st.error(f"❌ 資料庫連接異常: {db_test.message}")
1298
-
1299
- except Exception as e:
1300
- st.error(f"❌ 資料庫檢查失敗: {str(e)}")
1301
-
1302
- @staticmethod
1303
- def _show_system_info():
1304
- """顯示系統資訊"""
1305
- st.markdown('<h3 class="sub-header">📋 系統資訊</h3>', unsafe_allow_html=True)
1306
-
1307
- info_data = {
1308
- "系統名稱": "KSTools License Manager",
1309
- "版本": "1.0.0",
1310
- "API 基礎URL": Config.API_BASE,
1311
- "開發團隊": "KSTools Team",
1312
- "最後更新": "2024-09-09",
1313
- "Streamlit 版本": st.__version__,
1314
- "Python 版本": f"{os.sys.version_info.major}.{os.sys.version_info.minor}.{os.sys.version_info.micro}"
1315
- }
1316
-
1317
- col1, col2 = st.columns(2)
1318
-
1319
- for i, (key, value) in enumerate(info_data.items()):
1320
- with col1 if i % 2 == 0 else col2:
1321
- st.metric(key, value)
1322
-
1323
- @staticmethod
1324
- def _show_system_tools():
1325
- """顯示系統工���"""
1326
- st.markdown('<h3 class="sub-header">🛠️ 系統工具</h3>', unsafe_allow_html=True)
1327
-
1328
- col1, col2, col3 = st.columns(3)
1329
-
1330
- with col1:
1331
- st.markdown("**🗑️ 資料清理**")
1332
- st.write("清理過期和無效的資料")
1333
-
1334
- if st.button("清理過期授權", use_container_width=True):
1335
- with st.spinner("執行清理..."):
1336
- # 這裡可以實作清理邏輯
1337
- time.sleep(2) # 模擬處理時間
1338
- st.success("清理完成!")
1339
-
1340
- with col2:
1341
- st.markdown("**📊 資料匯出**")
1342
- st.write("匯出授權和記錄資料")
1343
-
1344
- if st.button("匯出 CSV", use_container_width=True):
1345
- with st.spinner("準備匯出..."):
1346
- # 這裡可以實作匯出邏輯
1347
- time.sleep(2)
1348
- st.success("匯出完成!")
1349
-
1350
- with col3:
1351
- st.markdown("**🔄 快取管理**")
1352
- st.write("清除系統快取資料")
1353
-
1354
- if st.button("清除快取", use_container_width=True):
1355
- st.cache_data.clear()
1356
- st.success("快取已清除!")
1357
- st.rerun()
1358
-
1359
- @staticmethod
1360
- def _show_environment_check():
1361
- """顯示環境設定檢查"""
1362
- st.markdown('<h3 class="sub-header">🔐 環境設定檢查</h3>', unsafe_allow_html=True)
1363
-
1364
- env_vars = {
1365
- "SUPABASE_URL": os.getenv("SUPABASE_URL"),
1366
- "SUPABASE_SERVICE_KEY": "已設定" if os.getenv("SUPABASE_SERVICE_KEY") else "未設定",
1367
- "PORT": os.getenv("PORT", "7860"),
1368
- "API_BASE": Config.API_BASE
1369
- }
1370
-
1371
- for var_name, var_value in env_vars.items():
1372
- col1, col2 = st.columns([1, 2])
1373
-
1374
- with col1:
1375
- st.write(f"**{var_name}:**")
1376
-
1377
- with col2:
1378
- if var_value and var_value != "未設定":
1379
- if var_name == "SUPABASE_SERVICE_KEY":
1380
- st.success("✅ 已設定")
1381
- elif var_name == "SUPABASE_URL" and var_value.startswith("http"):
1382
- st.success(f"✅ {var_value}")
1383
- else:
1384
- st.info(var_value)
1385
- else:
1386
- st.error("❌ 未設定")
1387
-
1388
- # 主應用程式
1389
- def main():
1390
- """主應用程式入口"""
1391
-
1392
- # 初始化 session state
1393
- if "current_page" not in st.session_state:
1394
- st.session_state.current_page = "📊 系統儀表板"
1395
-
1396
- # 側邊欄導航
1397
- with st.sidebar:
1398
- st.markdown("### 🔑 KSTools License Manager")
1399
- st.markdown("**現代化管理系統**")
1400
- st.markdown("---")
1401
-
1402
- # 導航選單
1403
- pages = [
1404
- "📊 系統儀表板",
1405
- "👥 用戶管理",
1406
- "📜 授權記錄",
1407
- "⚙️ 系統設定"
1408
- ]
1409
-
1410
- selected_page = st.radio(
1411
- "選擇頁面",
1412
- pages,
1413
- key="navigation",
1414
- index=pages.index(st.session_state.current_page) if st.session_state.current_page in pages else 0
1415
- )
1416
-
1417
- st.session_state.current_page = selected_page
1418
-
1419
- st.markdown("---")
1420
-
1421
- # 系統狀態指示器
1422
- st.markdown("**🚦 系統狀態**")
1423
-
1424
- # 快速狀態檢查 (減少頻率)
1425
- if "last_health_check" not in st.session_state or \
1426
- time.time() - st.session_state.get("last_health_check", 0) > 30: # 30秒檢查一次
1427
- try:
1428
- health_response = ApiClient.get("/health")
1429
- st.session_state.api_status = health_response.success
1430
- st.session_state.last_health_check = time.time()
1431
- except:
1432
- st.session_state.api_status = False
1433
- st.session_state.last_health_check = time.time()
1434
-
1435
- # 顯示快取的狀態
1436
- if st.session_state.get("api_status", False):
1437
- st.success("🟢 API 服務正常")
1438
- else:
1439
- st.error("🔴 API 無法連接")
1440
-
1441
- st.markdown("---")
1442
-
1443
- # 快速操作
1444
- st.markdown("**⚡ 快速操作**")
1445
-
1446
- if st.button("🔄 重新整理資料", use_container_width=True):
1447
- st.cache_data.clear()
1448
- st.success("✅ 快取已清除")
1449
- time.sleep(1)
1450
- st.rerun()
1451
-
1452
- if st.button("📊 API 文檔", use_container_width=True):
1453
- st.markdown(f"[📖 開啟 API 文檔]({Config.API_BASE.replace('/api', '')}/api/docs)")
1454
-
1455
- # 版本資訊
1456
- st.markdown("---")
1457
- st.markdown("**📦 版本資訊**")
1458
- st.caption("KSTools License Manager v1.0")
1459
- st.caption("Powered by Streamlit & FastAPI")
1460
-
1461
- # 主內容區域 - 根據選擇的頁���顯示內容
1462
- try:
1463
- if selected_page == "📊 系統儀表板":
1464
- DashboardPage.show()
1465
- elif selected_page == "👥 用戶管理":
1466
- UserManagementPage.show()
1467
- elif selected_page == "📜 授權記錄":
1468
- LicenseLogsPage.show()
1469
- elif selected_page == "⚙️ 系統設定":
1470
- SystemSettingsPage.show()
1471
- else:
1472
- st.error("未知的頁面")
1473
-
1474
- except Exception as e:
1475
- st.error(f"載入頁面時發生錯誤: {str(e)}")
1476
- st.exception(e)
1477
-
1478
- # 應用程式啟動
1479
- if __name__ == "__main__":
1480
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/config.js CHANGED
@@ -3,33 +3,35 @@
3
  * Loads configuration from backend API for security
4
  */
5
 
6
- // 先設定預設值,然後從 API 動態載入
7
  window.APP_CONFIG = {
8
- // 預設配置(會被 API 覆蓋)
9
- SUPABASE_URL: '',
10
- SUPABASE_ANON_KEY: '',
11
  APP_NAME: 'KSTools License Manager',
12
- VERSION: '1.0.0'
 
 
 
 
 
13
  };
14
 
15
  // 從 API 動態載入配置
16
  async function loadConfig() {
17
  try {
18
- // 在 HF Spaces 中,後端 API 運行在 port 8000
19
- const apiUrl = window.location.hostname.includes('hf.space') || window.location.hostname.includes('huggingface.co')
20
- ? `${window.location.protocol}//${window.location.hostname}:8000/api/frontend-config`
21
- : '/api/frontend-config';
22
-
23
- const response = await fetch(apiUrl);
24
  if (response.ok) {
25
  const config = await response.json();
26
  window.APP_CONFIG = { ...window.APP_CONFIG, ...config };
27
- console.log('配置已從 API 載入');
 
28
  } else {
29
- console.warn('無法從 API 載入配置,使用預設值');
 
30
  }
31
  } catch (error) {
32
- console.warn('載入配置時出錯,使用預設值:', error);
 
33
  }
34
  }
35
 
 
3
  * Loads configuration from backend API for security
4
  */
5
 
6
+ // 應用程式配置(從 API 動態載入
7
  window.APP_CONFIG = {
8
+ // 應用程式資訊
 
 
9
  APP_NAME: 'KSTools License Manager',
10
+ VERSION: '2.0.0',
11
+ // Supabase 配置(將從 API 載入)
12
+ SUPABASE_URL: null,
13
+ SUPABASE_ANON_KEY: null,
14
+ // 載入狀態
15
+ CONFIG_LOADED: false
16
  };
17
 
18
  // 從 API 動態載入配置
19
  async function loadConfig() {
20
  try {
21
+ // 在統一的 FastAPI 服務中,使用相對路徑
22
+ const response = await fetch('/api/frontend-config');
 
 
 
 
23
  if (response.ok) {
24
  const config = await response.json();
25
  window.APP_CONFIG = { ...window.APP_CONFIG, ...config };
26
+ window.APP_CONFIG.CONFIG_LOADED = true;
27
+ console.log('✅ 配置已從 API 載入');
28
  } else {
29
+ console.warn('⚠️ 無法從 API 載入配置,請檢查後端服務');
30
+ window.APP_CONFIG.CONFIG_LOADED = false;
31
  }
32
  } catch (error) {
33
+ console.error('載入配置時出錯:', error);
34
+ window.APP_CONFIG.CONFIG_LOADED = false;
35
  }
36
  }
37
 
frontend/js/api.js CHANGED
@@ -14,11 +14,9 @@ class ApiClient {
14
 
15
  if (host === 'localhost' || host === '127.0.0.1') {
16
  return `${protocol}//${host}:8000`;
17
- } else if (host.includes('hf.space') || host.includes('huggingface.co')) {
18
- // Hugging Face Spaces 環境,API 運行在 port 8000
19
- return `${protocol}//${host}:8000`;
20
  } else {
21
- return `${protocol}//${host}`;
 
22
  }
23
  }
24
 
 
14
 
15
  if (host === 'localhost' || host === '127.0.0.1') {
16
  return `${protocol}//${host}:8000`;
 
 
 
17
  } else {
18
+ // 在 HF Spaces 中使用相對路徑,統一服務在 port 7860
19
+ return '';
20
  }
21
  }
22
 
frontend/js/auth.js CHANGED
@@ -44,8 +44,8 @@ class AuthManager {
44
  throw new Error('Supabase configuration missing');
45
  }
46
 
47
- if (config.url === 'https://your-project.supabase.co') {
48
- throw new Error('Please update config.js with your actual Supabase URL');
49
  }
50
 
51
  this.supabase = supabase.createClient(config.url, config.anonKey);
 
44
  throw new Error('Supabase configuration missing');
45
  }
46
 
47
+ if (!config.url || config.url.includes('your-project') || !config.anonKey) {
48
+ throw new Error('Supabase configuration not properly loaded from API');
49
  }
50
 
51
  this.supabase = supabase.createClient(config.url, config.anonKey);
start.sh CHANGED
@@ -1,41 +1,18 @@
1
  #!/bin/bash
2
 
 
 
3
  echo "🚀 Starting KSTools License Manager..."
4
 
5
- # 啟動 FastAPI (背景執行)
6
- echo "🔧 Starting FastAPI backend..."
7
  cd /app
8
- python -m app.main &
9
- FASTAPI_PID=$!
10
 
11
- # 等待 FastAPI 啟動
12
- echo " Waiting for FastAPI to start..."
13
- sleep 10
 
 
 
14
 
15
- # 檢查 FastAPI 是否正常啟動 (重試機制)
16
- echo "🔍 Checking FastAPI health..."
17
- for i in {1..5}; do
18
- if curl -f http://localhost:8000/health > /dev/null 2>&1; then
19
- echo "✅ FastAPI backend is running on port 8000"
20
- break
21
- else
22
- echo "⏳ Attempt $i/5: Waiting for FastAPI..."
23
- sleep 3
24
- fi
25
-
26
- if [ $i -eq 5 ]; then
27
- echo "❌ FastAPI backend failed to start after 5 attempts"
28
- echo "📝 Checking if process is running..."
29
- if kill -0 $FASTAPI_PID 2>/dev/null; then
30
- echo "🔄 FastAPI process is running, continuing anyway..."
31
- else
32
- echo "💀 FastAPI process died, exiting..."
33
- exit 1
34
- fi
35
- fi
36
- done
37
-
38
- # 啟動前端靜態檔案伺服器
39
- echo "🎨 Starting HTML/CSS/JS frontend..."
40
- cd /app/frontend
41
- python -m http.server 7860 --bind 0.0.0.0
 
1
  #!/bin/bash
2
 
3
+ echo "===== Application Startup at $(date) ====="
4
+ echo ""
5
  echo "🚀 Starting KSTools License Manager..."
6
 
7
+ # 切換到應用目錄
 
8
  cd /app
 
 
9
 
10
+ # 使用統一服務器啟動前後端
11
+ echo "🌐 Starting unified frontend + backend server..."
12
+ echo "📡 Port: 7860"
13
+ echo "🔗 Frontend: HTML/CSS/JS"
14
+ echo "⚙️ Backend: FastAPI"
15
+ echo ""
16
 
17
+ # 執行統一服務器
18
+ python3 unified_server.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
unified_server.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ 統一的 FastAPI 服務器,同時處理前端和 API 請求
4
+ 專為 Hugging Face Spaces 設計
5
+ """
6
+
7
+ import uvicorn
8
+ from fastapi import FastAPI, Request
9
+ from fastapi.staticfiles import StaticFiles
10
+ from fastapi.responses import FileResponse
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ import os
13
+ from pathlib import Path
14
+
15
+ # 導入 API 路由模組,而不是整個 app
16
+ from app.api import license, hardware
17
+ from app import config_endpoint
18
+
19
+ # 創建主應用
20
+ app = FastAPI(
21
+ title="KSTools License Manager",
22
+ description="統一的前後端服務",
23
+ version="2.0.0",
24
+ docs_url="/api/docs",
25
+ redoc_url="/api/redoc"
26
+ )
27
+
28
+ # 添加 CORS 中間件
29
+ app.add_middleware(
30
+ CORSMiddleware,
31
+ allow_origins=["*"],
32
+ allow_credentials=True,
33
+ allow_methods=["*"],
34
+ allow_headers=["*"],
35
+ )
36
+
37
+ # 直接包含 API 路由 (避免掛載衝突)
38
+ app.include_router(license.router, prefix="/api", tags=["license"])
39
+ app.include_router(hardware.router, prefix="/api", tags=["hardware"])
40
+ app.include_router(config_endpoint.router, prefix="/api", tags=["config"])
41
+
42
+ # 靜態檔案路徑
43
+ frontend_path = Path(__file__).parent / "frontend"
44
+
45
+ # 處理靜態檔案
46
+ @app.get("/css/{filename:path}")
47
+ async def serve_css(filename: str):
48
+ file_path = frontend_path / "css" / filename
49
+ if file_path.exists():
50
+ return FileResponse(file_path, media_type="text/css")
51
+ return {"error": "File not found"}, 404
52
+
53
+ @app.get("/js/{filename:path}")
54
+ async def serve_js(filename: str):
55
+ file_path = frontend_path / "js" / filename
56
+ if file_path.exists():
57
+ return FileResponse(file_path, media_type="application/javascript")
58
+ return {"error": "File not found"}, 404
59
+
60
+ @app.get("/config.js")
61
+ async def serve_config():
62
+ file_path = frontend_path / "config.js"
63
+ if file_path.exists():
64
+ return FileResponse(file_path, media_type="application/javascript")
65
+ return {"error": "File not found"}, 404
66
+
67
+ # 前端頁面路由
68
+ @app.get("/")
69
+ async def serve_index():
70
+ return FileResponse(frontend_path / "index.html")
71
+
72
+ @app.get("/login.html")
73
+ async def serve_login():
74
+ return FileResponse(frontend_path / "login.html")
75
+
76
+ @app.get("/index.html")
77
+ async def serve_index_html():
78
+ return FileResponse(frontend_path / "index.html")
79
+
80
+ # API 健康檢查端點
81
+ @app.get("/api/health")
82
+ async def api_health_check():
83
+ return {
84
+ "status": "healthy",
85
+ "service": "KSTools License Manager API",
86
+ "frontend": "HTML/CSS/JS",
87
+ "backend": "FastAPI",
88
+ "port": 7860
89
+ }
90
+
91
+ # 根路徑健康檢查
92
+ @app.get("/health")
93
+ async def health_check():
94
+ return {
95
+ "status": "healthy",
96
+ "service": "KSTools License Manager (Unified)",
97
+ "frontend": "HTML/CSS/JS",
98
+ "backend": "FastAPI",
99
+ "port": 7860
100
+ }
101
+
102
+ # 根 API 資訊
103
+ @app.get("/api")
104
+ async def api_root():
105
+ return {
106
+ "message": "KSTools License Manager API",
107
+ "status": "running",
108
+ "version": "2.0.0",
109
+ "docs": "/api/docs"
110
+ }
111
+
112
+ # 通配符路由最後處理 (避免攔截 API)
113
+ @app.get("/{full_path:path}")
114
+ async def serve_spa_routes(full_path: str):
115
+ # 檢查是否是靜態檔案
116
+ file_path = frontend_path / full_path
117
+ if file_path.exists() and file_path.is_file():
118
+ return FileResponse(file_path)
119
+
120
+ # 對於所有其他路徑,返回 index.html (SPA 路由)
121
+ return FileResponse(frontend_path / "index.html")
122
+
123
+ if __name__ == "__main__":
124
+ port = int(os.environ.get("PORT", 7860))
125
+ print(f"🚀 Starting unified KSTools License Manager on port {port}")
126
+ print(f"📁 Frontend path: {frontend_path}")
127
+ print(f"🔗 Access: http://0.0.0.0:{port}")
128
+
129
+ uvicorn.run(
130
+ "unified_server:app",
131
+ host="0.0.0.0",
132
+ port=port,
133
+ reload=False,
134
+ log_level="info"
135
+ )