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