From 69447981bc45928e4f3ebef93a3caa9adedd3b80 Mon Sep 17 00:00:00 2001 From: swpa Date: Mon, 15 Jun 2026 18:04:20 +0800 Subject: [PATCH] feat: implement WebSocket Log Streamer with UI toggle switch --- PROJECT_STATE.md | 19 ++-- all_code.txt | 273 ++++++++++++++++++++++++++++++++++++++++++++--- index.html | 15 +++ logger.py | 9 ++ main.py | 6 +- routers/logs.py | 79 ++++++++++++++ static/app.js | 106 +++++++++++++++++- static/style.css | 35 ++++++ 8 files changed, 514 insertions(+), 28 deletions(-) create mode 100644 routers/logs.py diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 1b5f0a2..76d8958 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -34,15 +34,13 @@ ### Phase 4: 自動化防護、進階管理與指令精準度 (Automation & Advanced Management) - [x] **智能視覺診斷 (Visual Diagnostics)**: - CM 一鍵診斷中心:整合基礎狀態、PHY 射頻指標與 OFDM MER 頻譜。引入 Chart.js 將 ASCII 報表轉化為紅黃綠狀態圖與長條圖。 -- [ ] **Diff 引擎指令精準度強化 (Diff Logic Hardening)**: - - 重新 Review `generate_diff_commands` 演算法。 - - 實作「指令截斷機制」,確保生成 `no` 移除指令時,能精準剝離多餘的 Value 或參數,避免 CMTS 拒絕執行或引發非預期刪除。 -- [ ] **自動備份攔截防護 (Auto-Backup Interceptor)**: - - 在執行任何 `generate_cli` (寫入設備變更) 的 API 之前,實作強制背景備份 `running-config` 的防呆機制。 - - 將此類備份標記為 `is_auto = true`,確保每次人為變更前都有絕對的還原點。 -- [ ] **雙軌制備份保留策略 (Dual-Track Retention Policy)**: - - **自動快照滾動 (Rolling Window)**:限制每台設備最多保留 30 份自動快照,採用 FIFO (先進先出) 機制自動清理舊資料。 - - **手動快照配額 (Manual Quota)**:限制每台設備最多保留 20 份手動快照,達上限時於前端 UI 提示使用者進行清理,防止資料庫無限膨脹。 +- [x] **備份保留策略 (Retention Policy)**: + - **手動快照配額 (Manual Quota)**:限制每台設備最多保留 20 份手動快照,達上限時,採用 FIFO (先進先出) 機制自動清理舊資料,防止資料庫無限膨脹。 +- [ ] **系統日誌動態儀表板 (Log Viewer UI)** : + - 基於 FastAPI 實作一個 WebSocket Log Streamer。 + 1. **Custom Log Handler**: 在現有的 Python `logging` 模組中,撰寫一個自訂的 Handler,能夠攔截系統的 Log 訊息(包含 ANSI 色碼)。 + 2. **WebSocket Endpoint**: 建立一個 FastAPI WebSocket 路由 `/ws/logs`。 + 3. **Broadcaster**: 實作一個簡單的機制,當 Custom Log Handler 收到新日誌時,能非同步地將訊息推播給所有連線中的 WebSocket 客戶端。 ### Phase 5: RPD 快速擴容與部署精靈 (Rapid RPD Provisioning Wizard) - [ ] **RPD 樣板克隆引擎 (Template Cloning)**: @@ -51,6 +49,9 @@ - 透過底層 Tree 架構,完整複製複雜的 RF 與通道設定,並提供 UI 介面供使用者修改唯一識別碼 (如 MAC Address、RPD Name)。 - [ ] **安全寫入與校驗 (Safe Provisioning)**: - 結合 Phase 4 的防護機制,在將全新 RPD 配置寫入 CMTS 前,進行參數衝突檢查(避免 MAC 或 IP 重複),實現零錯誤的設備擴容。 +- [ ] **Diff 引擎指令精準度強化 (Diff Logic Hardening)**: + - 重新 Review `generate_diff_commands` 演算法。 + - 實作「指令截斷機制」,確保生成 `no` 移除指令時,能精準剝離多餘的 Value 或參數,避免 CMTS 拒絕執行或引發非預期刪除。 --- diff --git a/all_code.txt b/all_code.txt index 58c1074..81525f0 100644 --- a/all_code.txt +++ b/all_code.txt @@ -1226,6 +1226,21 @@ FILE: index.html ⏳ 正在載入日誌設定... + + +
+

+ 🎙️ 系統實時日誌 (System Live Logs) + +
+ 狀態:已暫停 ⚪ + +
+

+ + +
+
@@ -1886,14 +1901,17 @@ from fastapi.responses import HTMLResponse from contextlib import asynccontextmanager from starlette.middleware.base import BaseHTTPMiddleware # 🌟 新增引入 import database +import asyncio # 引入我們剛剛拆分出來的路由模組 -from routers import query, config, terminal, lock, leaf_options, backup, diagnostics, auth +from routers import query, config, terminal, lock, leaf_options, backup, diagnostics, auth, logs @asynccontextmanager async def lifespan(app: FastAPI): # Startup await database.init_db_pool() + # 🌟 新增:初始化 Log Broadcaster 的 Event Loop 參考,確保跨執行緒排程安全 + logs.log_broadcaster.loop = asyncio.get_running_loop() yield # Shutdown await database.close_db_pool() @@ -1934,6 +1952,7 @@ app.include_router(auth.router, prefix="/api/v1") # WebSocket 通常獨立於 API 版本之外,所以不加前綴 app.include_router(terminal.router) +app.include_router(logs.router) # 🌟 新增:掛載日誌 WebSocket 路由 # 根目錄路由:回傳前端 UI @app.get("/", response_class=HTMLResponse, tags=["UI"]) @@ -2012,6 +2031,8 @@ MANAGED_LOGGERS = [ "app.diagnostics", # CM 診斷 "app.backup", # 備份與還原 "app.ssh" # SSH 連線引擎 + "app.ssh" # SSH 連線引擎 + "app.auth" # 🌟 新增:上帝模式與安全授權 ] def setup_logger(name: str, level=logging.INFO): @@ -2047,6 +2068,12 @@ def set_log_level(name: str, level_str: str): return True return False +def register_websocket_handler(handler: logging.Handler): + """🌟 新增:註冊自訂 Handler 到所有受管控的 Logger,並防止重複添加""" + for name in MANAGED_LOGGERS: + logger = logging.getLogger(name) + if handler not in logger.handlers: + logger.addHandler(handler) ================================================================================ FILE: PROJECT_STATE.md @@ -2087,15 +2114,13 @@ FILE: PROJECT_STATE.md ### Phase 4: 自動化防護、進階管理與指令精準度 (Automation & Advanced Management) - [x] **智能視覺診斷 (Visual Diagnostics)**: - CM 一鍵診斷中心:整合基礎狀態、PHY 射頻指標與 OFDM MER 頻譜。引入 Chart.js 將 ASCII 報表轉化為紅黃綠狀態圖與長條圖。 -- [ ] **Diff 引擎指令精準度強化 (Diff Logic Hardening)**: - - 重新 Review `generate_diff_commands` 演算法。 - - 實作「指令截斷機制」,確保生成 `no` 移除指令時,能精準剝離多餘的 Value 或參數,避免 CMTS 拒絕執行或引發非預期刪除。 -- [ ] **自動備份攔截防護 (Auto-Backup Interceptor)**: - - 在執行任何 `generate_cli` (寫入設備變更) 的 API 之前,實作強制背景備份 `running-config` 的防呆機制。 - - 將此類備份標記為 `is_auto = true`,確保每次人為變更前都有絕對的還原點。 -- [ ] **雙軌制備份保留策略 (Dual-Track Retention Policy)**: - - **自動快照滾動 (Rolling Window)**:限制每台設備最多保留 30 份自動快照,採用 FIFO (先進先出) 機制自動清理舊資料。 - - **手動快照配額 (Manual Quota)**:限制每台設備最多保留 20 份手動快照,達上限時於前端 UI 提示使用者進行清理,防止資料庫無限膨脹。 +- [x] **備份保留策略 (Retention Policy)**: + - **手動快照配額 (Manual Quota)**:限制每台設備最多保留 20 份手動快照,達上限時,採用 FIFO (先進先出) 機制自動清理舊資料,防止資料庫無限膨脹。 +- [ ] **系統日誌動態儀表板 (Log Viewer UI)** : + - 基於 FastAPI 實作一個 WebSocket Log Streamer。 + 1. **Custom Log Handler**: 在現有的 Python `logging` 模組中,撰寫一個自訂的 Handler,能夠攔截系統的 Log 訊息(包含 ANSI 色碼)。 + 2. **WebSocket Endpoint**: 建立一個 FastAPI WebSocket 路由 `/ws/logs`。 + 3. **Broadcaster**: 實作一個簡單的機制,當 Custom Log Handler 收到新日誌時,能非同步地將訊息推播給所有連線中的 WebSocket 客戶端。 ### Phase 5: RPD 快速擴容與部署精靈 (Rapid RPD Provisioning Wizard) - [ ] **RPD 樣板克隆引擎 (Template Cloning)**: @@ -2104,6 +2129,9 @@ FILE: PROJECT_STATE.md - 透過底層 Tree 架構,完整複製複雜的 RF 與通道設定,並提供 UI 介面供使用者修改唯一識別碼 (如 MAC Address、RPD Name)。 - [ ] **安全寫入與校驗 (Safe Provisioning)**: - 結合 Phase 4 的防護機制,在將全新 RPD 配置寫入 CMTS 前,進行參數衝突檢查(避免 MAC 或 IP 重複),實現零錯誤的設備擴容。 +- [ ] **Diff 引擎指令精準度強化 (Diff Logic Hardening)**: + - 重新 Review `generate_diff_commands` 演算法。 + - 實作「指令截斷機制」,確保生成 `no` 移除指令時,能精準剝離多餘的 Value 或參數,避免 CMTS 拒絕執行或引發非預期刪除。 --- @@ -2417,6 +2445,10 @@ let idleTimer; const IDLE_TIMEOUT = 300000; // 5 分鐘 (毫秒) let globalSSE = null; +let logTerm = null; +let logWs = null; +let isLogEnabled = false; // 預設關閉,不主動消耗資源 + // 初始化全域 SSE 監聽器 (設備級別) function initGlobalSSE() { // 🌟 新增防線:如果尚未連線,強制關閉舊的 SSE 並退出 @@ -3600,9 +3632,102 @@ async function switchFilterMode() { await openSystemSettings(); } -// 開啟系統設定並讀取目前的過濾器清單 +function initLogTerminal() { + const container = document.getElementById('log-terminal-container'); + if (!container || logTerm) return; + + logTerm = new Terminal({ + cursorBlink: false, + disableStdin: true, + theme: { background: '#1e1e1e', foreground: '#e0e0e0' }, + fontFamily: 'Courier New, monospace', + fontSize: 13, + scrollback: 1000, + cols: 250 // 確保不折行 + }); + logTerm.open(container); + logTerm.writeln('\x1b[33m[System] Live Log Terminal Ready. Click "啟動監聽" to start streaming.\x1b[0m\r\n'); +} + +function toggleLogStream() { + isLogEnabled = !isLogEnabled; + const btn = document.getElementById('btnToggleLog'); + + if (isLogEnabled) { + initLogTerminal(); + connectLogWebSocket(); + if (btn) { + btn.textContent = '⏸️ 暫停監聽'; + btn.className = 'btn-modern btn-clear'; + } + } else { + disconnectLogWebSocket(); + if (btn) { + btn.textContent = '🔌 啟動監聽'; + btn.className = 'btn-modern btn-connect'; + } + } +} + +function connectLogWebSocket() { + if (!isLogEnabled) return; + if (logWs && logWs.readyState === WebSocket.OPEN) return; + + const statusEl = document.getElementById('log-ws-status'); + if (statusEl) { + statusEl.textContent = '狀態:正在連線... 🟡'; + statusEl.style.color = '#f39c12'; + } + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/ws/logs`; + + logWs = new WebSocket(wsUrl); + + logWs.onopen = () => { + if (statusEl) { + statusEl.textContent = '狀態:實時監聽中 🟢'; + statusEl.style.color = '#27ae60'; + } + }; + + logWs.onmessage = (event) => { + if (logTerm && isLogEnabled) { + const formattedLog = event.data.replace(/\n/g, '\r\n') + '\r\n'; + logTerm.write(formattedLog); + } + }; + + logWs.onclose = () => { + const statusEl = document.getElementById('log-ws-status'); + if (isLogEnabled) { + if (statusEl) { + statusEl.textContent = '狀態:連線中斷,嘗試重連... 🔴'; + statusEl.style.color = '#c0392b'; + } + setTimeout(connectLogWebSocket, 5000); + } else { + if (statusEl) { + statusEl.textContent = '狀態:已暫停 ⚪'; + statusEl.style.color = '#7f8c8d'; + } + } + }; +} + +function disconnectLogWebSocket() { + if (logWs) { + logWs.close(); + logWs = null; + } +} + +// --- 3. 確保 openSystemSettings 正常初始化 --- async function openSystemSettings() { - // 🌟 動態抓取過濾器模式 + setTimeout(() => { + initLogTerminal(); + }, 50); + const modeSelect = document.getElementById('filter-mode-select'); const mode = modeSelect ? modeSelect.value : 'running'; @@ -3613,7 +3738,6 @@ async function openSystemSettings() { console.error("讀取設定失敗:", error); } - // 🌟 新增:同時載入日誌等級 loadLogLevels(); } @@ -3630,7 +3754,8 @@ async function loadLogLevels() { 'app.scraper': '🕷️ 爬蟲與快取模組 (Scraper)', 'app.diagnostics': '🩺 CM 診斷模組 (Diagnostics)', 'app.backup': '💾 備份與還原模組 (Backup)', - 'app.ssh': '🔌 SSH 連線引擎 (SSH Engine)' + 'app.ssh': '🔌 SSH 連線引擎 (SSH Engine)', + 'app.auth': '🔐 安全授權模組 (Auth)' // 🌟 新增這行 }; for (const [module, level] of Object.entries(res.data)) { @@ -4604,6 +4729,7 @@ window.scanMissingOptions = scanMissingOptions; window.clearOptionsCache = clearOptionsCache; window.previewNodeCLI = previewNodeCLI; window.switchFilterMode = switchFilterMode; +window.toggleLogStream = toggleLogStream; // --- 樹狀圖展開與編輯操作 --- window.expandAll = expandAll; @@ -5141,6 +5267,41 @@ button:hover { background-color: #3498db; } contain-intrinsic-size: 0 24px; } +/* ============================================================================ + 🎙️ 系統實時日誌終端機 - 滾動條隔離優化 (Scrollbar Isolation - 修正版) + ============================================================================ */ +#log-terminal-container .xterm { + width: 100% !important; +} + +#log-terminal-container .xterm-screen { + /* 🌟 關鍵:限制文字區最大寬度,扣除右側垂直滾動條的寬度 */ + max-width: calc(100% - 18px) !important; + + /* 🌟 關鍵:只允許水平滾動,強制關閉垂直滾動,消除多餘的藍灰色軌道 */ + overflow-x: auto !important; + overflow-y: hidden !important; +} + +/* 讓水平滾動條呈現精美的扁平化現代風格 */ +#log-terminal-container .xterm-screen::-webkit-scrollbar { + height: 8px; +} + +#log-terminal-container .xterm-screen::-webkit-scrollbar-track { + background: #1e1e1e; + border-radius: 4px; +} + +#log-terminal-container .xterm-screen::-webkit-scrollbar-thumb { + background: #475569; + border-radius: 4px; +} + +#log-terminal-container .xterm-screen::-webkit-scrollbar-thumb:hover { + background: #64748b; +} + .unpinned-icon:hover { opacity: 1 !important; transform: scale(1.2); } ================================================================================ @@ -7867,6 +8028,90 @@ async def get_cm_diagnostics( logger.error("CM Diagnostics Error: " + str(e)) raise HTTPException(status_code=500, detail="設備連線或查詢失敗: " + str(e)) +================================================================================ +FILE: routers/logs.py +================================================================================ +# --- routers/logs.py --- +import asyncio +import logging +from fastapi import APIRouter, WebSocket, WebSocketDisconnect +from logger import ColoredFormatter, register_websocket_handler + +router = APIRouter() + +class LogBroadcaster: + """管理所有活躍 WebSocket 連線的廣播器 (Connection Manager)""" + def __init__(self): + self.active_connections: set[WebSocket] = set() + self.loop: asyncio.AbstractEventLoop = None + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_connections.add(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.discard(websocket) + + async def broadcast(self, message: str): + """非同步推播日誌給所有連線中的客戶端""" + if not self.active_connections: + return + + # 併發發送,並透過 return_exceptions=True 確保單一連線異常不影響其他客戶端 + tasks = [self._safe_send(conn, message) for conn in self.active_connections] + await asyncio.gather(*tasks, return_exceptions=True) + + async def _safe_send(self, websocket: WebSocket, message: str): + try: + await websocket.send_text(message) + except Exception: + self.disconnect(websocket) + +# 建立全域廣播器實例 +log_broadcaster = LogBroadcaster() + +class WebSocketLogHandler(logging.Handler): + """自訂 Log Handler,攔截系統日誌並安全地派發至非同步廣播器""" + def __init__(self, broadcaster: LogBroadcaster): + super().__init__() + self.broadcaster = broadcaster + # 沿用系統 ColoredFormatter,完美保留 ANSI 色碼 + self.setFormatter(ColoredFormatter()) + + def emit(self, record): + try: + msg = self.format(record) + loop = self.broadcaster.loop + # 確保在 Event Loop 處於執行狀態時,安全地跨執行緒派發任務 + if loop and loop.is_running(): + loop.call_soon_threadsafe( + lambda: asyncio.create_task(self.broadcaster.broadcast(msg)) + ) + except Exception: + self.handleError(record) + +# 建立 Handler 實例並註冊至所有受管控的 Logger +websocket_log_handler = WebSocketLogHandler(log_broadcaster) +register_websocket_handler(websocket_log_handler) + +@router.websocket("/ws/logs") +async def websocket_logs(websocket: WebSocket): + """WebSocket 實時日誌串流端點""" + # 若尚未綁定 Event Loop,則於首次連線時動態綁定 + if not log_broadcaster.loop: + log_broadcaster.loop = asyncio.get_running_loop() + + await log_broadcaster.connect(websocket) + try: + # 保持連線,監聽客戶端斷線狀態 + while True: + await websocket.receive_text() + except WebSocketDisconnect: + log_broadcaster.disconnect(websocket) + except Exception: + log_broadcaster.disconnect(websocket) + + ================================================================================ FILE: routers/query.py ================================================================================ diff --git a/index.html b/index.html index 5ff202c..1f8c28b 100644 --- a/index.html +++ b/index.html @@ -449,6 +449,21 @@ ⏳ 正在載入日誌設定... + + +
+

+ 🎙️ 系統實時日誌 (System Live Logs) + +
+ 狀態:已暫停 ⚪ + +
+

+ + +
+
diff --git a/logger.py b/logger.py index 6c28593..e972326 100644 --- a/logger.py +++ b/logger.py @@ -27,6 +27,8 @@ MANAGED_LOGGERS = [ "app.diagnostics", # CM 診斷 "app.backup", # 備份與還原 "app.ssh" # SSH 連線引擎 + "app.ssh" # SSH 連線引擎 + "app.auth" # 🌟 新增:上帝模式與安全授權 ] def setup_logger(name: str, level=logging.INFO): @@ -61,3 +63,10 @@ def set_log_level(name: str, level_str: str): logging.getLogger(name).setLevel(level) return True return False + +def register_websocket_handler(handler: logging.Handler): + """🌟 新增:註冊自訂 Handler 到所有受管控的 Logger,並防止重複添加""" + for name in MANAGED_LOGGERS: + logger = logging.getLogger(name) + if handler not in logger.handlers: + logger.addHandler(handler) \ No newline at end of file diff --git a/main.py b/main.py index a9de604..f6860e8 100644 --- a/main.py +++ b/main.py @@ -6,14 +6,17 @@ from fastapi.responses import HTMLResponse from contextlib import asynccontextmanager from starlette.middleware.base import BaseHTTPMiddleware # 🌟 新增引入 import database +import asyncio # 引入我們剛剛拆分出來的路由模組 -from routers import query, config, terminal, lock, leaf_options, backup, diagnostics, auth +from routers import query, config, terminal, lock, leaf_options, backup, diagnostics, auth, logs @asynccontextmanager async def lifespan(app: FastAPI): # Startup await database.init_db_pool() + # 🌟 新增:初始化 Log Broadcaster 的 Event Loop 參考,確保跨執行緒排程安全 + logs.log_broadcaster.loop = asyncio.get_running_loop() yield # Shutdown await database.close_db_pool() @@ -54,6 +57,7 @@ app.include_router(auth.router, prefix="/api/v1") # WebSocket 通常獨立於 API 版本之外,所以不加前綴 app.include_router(terminal.router) +app.include_router(logs.router) # 🌟 新增:掛載日誌 WebSocket 路由 # 根目錄路由:回傳前端 UI @app.get("/", response_class=HTMLResponse, tags=["UI"]) diff --git a/routers/logs.py b/routers/logs.py new file mode 100644 index 0000000..92d7efb --- /dev/null +++ b/routers/logs.py @@ -0,0 +1,79 @@ +# --- routers/logs.py --- +import asyncio +import logging +from fastapi import APIRouter, WebSocket, WebSocketDisconnect +from logger import ColoredFormatter, register_websocket_handler + +router = APIRouter() + +class LogBroadcaster: + """管理所有活躍 WebSocket 連線的廣播器 (Connection Manager)""" + def __init__(self): + self.active_connections: set[WebSocket] = set() + self.loop: asyncio.AbstractEventLoop = None + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_connections.add(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.discard(websocket) + + async def broadcast(self, message: str): + """非同步推播日誌給所有連線中的客戶端""" + if not self.active_connections: + return + + # 併發發送,並透過 return_exceptions=True 確保單一連線異常不影響其他客戶端 + tasks = [self._safe_send(conn, message) for conn in self.active_connections] + await asyncio.gather(*tasks, return_exceptions=True) + + async def _safe_send(self, websocket: WebSocket, message: str): + try: + await websocket.send_text(message) + except Exception: + self.disconnect(websocket) + +# 建立全域廣播器實例 +log_broadcaster = LogBroadcaster() + +class WebSocketLogHandler(logging.Handler): + """自訂 Log Handler,攔截系統日誌並安全地派發至非同步廣播器""" + def __init__(self, broadcaster: LogBroadcaster): + super().__init__() + self.broadcaster = broadcaster + # 沿用系統 ColoredFormatter,完美保留 ANSI 色碼 + self.setFormatter(ColoredFormatter()) + + def emit(self, record): + try: + msg = self.format(record) + loop = self.broadcaster.loop + # 確保在 Event Loop 處於執行狀態時,安全地跨執行緒派發任務 + if loop and loop.is_running(): + loop.call_soon_threadsafe( + lambda: asyncio.create_task(self.broadcaster.broadcast(msg)) + ) + except Exception: + self.handleError(record) + +# 建立 Handler 實例並註冊至所有受管控的 Logger +websocket_log_handler = WebSocketLogHandler(log_broadcaster) +register_websocket_handler(websocket_log_handler) + +@router.websocket("/ws/logs") +async def websocket_logs(websocket: WebSocket): + """WebSocket 實時日誌串流端點""" + # 若尚未綁定 Event Loop,則於首次連線時動態綁定 + if not log_broadcaster.loop: + log_broadcaster.loop = asyncio.get_running_loop() + + await log_broadcaster.connect(websocket) + try: + # 保持連線,監聽客戶端斷線狀態 + while True: + await websocket.receive_text() + except WebSocketDisconnect: + log_broadcaster.disconnect(websocket) + except Exception: + log_broadcaster.disconnect(websocket) diff --git a/static/app.js b/static/app.js index 196af11..68f5d3d 100644 --- a/static/app.js +++ b/static/app.js @@ -56,6 +56,10 @@ let idleTimer; const IDLE_TIMEOUT = 300000; // 5 分鐘 (毫秒) let globalSSE = null; +let logTerm = null; +let logWs = null; +let isLogEnabled = false; // 預設關閉,不主動消耗資源 + // 初始化全域 SSE 監聽器 (設備級別) function initGlobalSSE() { // 🌟 新增防線:如果尚未連線,強制關閉舊的 SSE 並退出 @@ -1239,9 +1243,102 @@ async function switchFilterMode() { await openSystemSettings(); } -// 開啟系統設定並讀取目前的過濾器清單 +function initLogTerminal() { + const container = document.getElementById('log-terminal-container'); + if (!container || logTerm) return; + + logTerm = new Terminal({ + cursorBlink: false, + disableStdin: true, + theme: { background: '#1e1e1e', foreground: '#e0e0e0' }, + fontFamily: 'Courier New, monospace', + fontSize: 13, + scrollback: 1000, + cols: 250 // 確保不折行 + }); + logTerm.open(container); + logTerm.writeln('\x1b[33m[System] Live Log Terminal Ready. Click "啟動監聽" to start streaming.\x1b[0m\r\n'); +} + +function toggleLogStream() { + isLogEnabled = !isLogEnabled; + const btn = document.getElementById('btnToggleLog'); + + if (isLogEnabled) { + initLogTerminal(); + connectLogWebSocket(); + if (btn) { + btn.textContent = '⏸️ 暫停監聽'; + btn.className = 'btn-modern btn-clear'; + } + } else { + disconnectLogWebSocket(); + if (btn) { + btn.textContent = '🔌 啟動監聽'; + btn.className = 'btn-modern btn-connect'; + } + } +} + +function connectLogWebSocket() { + if (!isLogEnabled) return; + if (logWs && logWs.readyState === WebSocket.OPEN) return; + + const statusEl = document.getElementById('log-ws-status'); + if (statusEl) { + statusEl.textContent = '狀態:正在連線... 🟡'; + statusEl.style.color = '#f39c12'; + } + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/ws/logs`; + + logWs = new WebSocket(wsUrl); + + logWs.onopen = () => { + if (statusEl) { + statusEl.textContent = '狀態:實時監聽中 🟢'; + statusEl.style.color = '#27ae60'; + } + }; + + logWs.onmessage = (event) => { + if (logTerm && isLogEnabled) { + const formattedLog = event.data.replace(/\n/g, '\r\n') + '\r\n'; + logTerm.write(formattedLog); + } + }; + + logWs.onclose = () => { + const statusEl = document.getElementById('log-ws-status'); + if (isLogEnabled) { + if (statusEl) { + statusEl.textContent = '狀態:連線中斷,嘗試重連... 🔴'; + statusEl.style.color = '#c0392b'; + } + setTimeout(connectLogWebSocket, 5000); + } else { + if (statusEl) { + statusEl.textContent = '狀態:已暫停 ⚪'; + statusEl.style.color = '#7f8c8d'; + } + } + }; +} + +function disconnectLogWebSocket() { + if (logWs) { + logWs.close(); + logWs = null; + } +} + +// --- 3. 確保 openSystemSettings 正常初始化 --- async function openSystemSettings() { - // 🌟 動態抓取過濾器模式 + setTimeout(() => { + initLogTerminal(); + }, 50); + const modeSelect = document.getElementById('filter-mode-select'); const mode = modeSelect ? modeSelect.value : 'running'; @@ -1252,7 +1349,6 @@ async function openSystemSettings() { console.error("讀取設定失敗:", error); } - // 🌟 新增:同時載入日誌等級 loadLogLevels(); } @@ -1269,7 +1365,8 @@ async function loadLogLevels() { 'app.scraper': '🕷️ 爬蟲與快取模組 (Scraper)', 'app.diagnostics': '🩺 CM 診斷模組 (Diagnostics)', 'app.backup': '💾 備份與還原模組 (Backup)', - 'app.ssh': '🔌 SSH 連線引擎 (SSH Engine)' + 'app.ssh': '🔌 SSH 連線引擎 (SSH Engine)', + 'app.auth': '🔐 安全授權模組 (Auth)' // 🌟 新增這行 }; for (const [module, level] of Object.entries(res.data)) { @@ -2243,6 +2340,7 @@ window.scanMissingOptions = scanMissingOptions; window.clearOptionsCache = clearOptionsCache; window.previewNodeCLI = previewNodeCLI; window.switchFilterMode = switchFilterMode; +window.toggleLogStream = toggleLogStream; // --- 樹狀圖展開與編輯操作 --- window.expandAll = expandAll; diff --git a/static/style.css b/static/style.css index 7cdce33..7247fc3 100644 --- a/static/style.css +++ b/static/style.css @@ -304,4 +304,39 @@ button:hover { background-color: #3498db; } contain-intrinsic-size: 0 24px; } +/* ============================================================================ + 🎙️ 系統實時日誌終端機 - 滾動條隔離優化 (Scrollbar Isolation - 修正版) + ============================================================================ */ +#log-terminal-container .xterm { + width: 100% !important; +} + +#log-terminal-container .xterm-screen { + /* 🌟 關鍵:限制文字區最大寬度,扣除右側垂直滾動條的寬度 */ + max-width: calc(100% - 18px) !important; + + /* 🌟 關鍵:只允許水平滾動,強制關閉垂直滾動,消除多餘的藍灰色軌道 */ + overflow-x: auto !important; + overflow-y: hidden !important; +} + +/* 讓水平滾動條呈現精美的扁平化現代風格 */ +#log-terminal-container .xterm-screen::-webkit-scrollbar { + height: 8px; +} + +#log-terminal-container .xterm-screen::-webkit-scrollbar-track { + background: #1e1e1e; + border-radius: 4px; +} + +#log-terminal-container .xterm-screen::-webkit-scrollbar-thumb { + background: #475569; + border-radius: 4px; +} + +#log-terminal-container .xterm-screen::-webkit-scrollbar-thumb:hover { + background: #64748b; +} + .unpinned-icon:hover { opacity: 1 !important; transform: scale(1.2); } \ No newline at end of file