diff --git a/all_code.txt b/all_code.txt
index b365aac..683cb89 100644
--- a/all_code.txt
+++ b/all_code.txt
@@ -3,28 +3,71 @@ PROJECT SOURCE CODE EXPORT (FULL)
================================================================================
+================================================================================
+FILE: .env.example
+================================================================================
+# --- .env.example ---
+# Database Configuration (請填入你的本地端或正式機設定)
+DB_NAME=cmts_nms
+DB_USER=
+DB_PASS=
+DB_HOST=127.0.0.1
+DB_PORT=5432
+
+# God Mode Secret
+GOD_MODE_SECRET=
+
+# Default CMTS Device
+DEFAULT_CMTS_HOST=
+DEFAULT_CMTS_USER=
+DEFAULT_CMTS_PASS=
+
+================================================================================
+FILE: .env
+================================================================================
+# --- .env ---
+# Database Configuration
+DB_NAME=cmts_nms
+DB_USER=swpa
+DB_PASS=swpa4920
+DB_HOST=127.0.0.1
+DB_PORT=5432
+
+# God Mode Secret
+GOD_MODE_SECRET=swpa@Serc0mm
+
+# Default CMTS Device (本地開發用,正式環境可留空)
+DEFAULT_CMTS_HOST=10.14.110.4
+DEFAULT_CMTS_USER=admin
+DEFAULT_CMTS_PASS=nsgadmin
+
================================================================================
FILE: database.py
================================================================================
import asyncpg
import json
import uuid
+import os
+from dotenv import load_dotenv # 🌟 1. 引入 load_dotenv
from typing import Dict, List, Optional, Any
from logger import get_logger
+# 🌟 2. 明確指示 Python 讀取同目錄下的 .env 檔案
+load_dotenv()
+
# ==========================================
# 💡 PostgreSQL 連線與操作 (高可用性版)
# ==========================================
logger = get_logger("app.database")
-# DB 設定 (從您的 init_db.py 中提取)
+# 🌟 2. 同步改用 os.getenv 讀取環境變數
DB_CONFIG = {
- "database": "cmts_nms",
- "user": "swpa",
- "password": "swpa4920",
- "host": "127.0.0.1",
- "port": "5432"
+ "database": os.getenv("DB_NAME", "cmts_nms"),
+ "user": os.getenv("DB_USER", "postgres"), # 本地開發常用的預設帳號,或留空 ""
+ "password": os.getenv("DB_PASS", ""), # 🌟 絕對機密:預設留空!
+ "host": os.getenv("DB_HOST", "127.0.0.1"),
+ "port": os.getenv("DB_PORT", "5432")
}
_pool: Optional[asyncpg.Pool] = None
@@ -371,12 +414,12 @@ def debug_print(msg: str):
if DEBUG_MODE:
print(msg)
-# 預設的 CMTS 連線樣板
+# 🌟 2. 淨化預設的 CMTS 連線樣板
CMTS_DEVICE = {
'device_type': 'cisco_ios',
- 'host': '10.14.110.4',
- 'username': 'admin',
- 'password': 'nsgadmin',
+ 'host': os.getenv("DEFAULT_CMTS_HOST", ""),
+ 'username': os.getenv("DEFAULT_CMTS_USER", ""),
+ 'password': os.getenv("DEFAULT_CMTS_PASS", ""),
'port': 22,
'fast_cli': False,
'global_delay_factor': 2
@@ -646,9 +689,10 @@ FILE: index.html
-
-
-
+
+
+
+
@@ -1238,17 +1282,22 @@ import asyncio
import asyncpg
import logging
import sys
+import os
+from dotenv import load_dotenv # 🌟 1. 引入 load_dotenv
+
+# 🌟 2. 明確指示 Python 讀取同目錄下的 .env 檔案
+load_dotenv()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
-# DB 設定
+# 🌟 2. 改用 os.getenv 讀取環境變數,並保留原本的設定作為安全預設值
DB_CONFIG = {
- "database": "cmts_nms",
- "user": "swpa",
- "password": "swpa4920",
- "host": "127.0.0.1",
- "port": "5432"
+ "database": os.getenv("DB_NAME", "cmts_nms"),
+ "user": os.getenv("DB_USER", "postgres"), # 本地開發常用的預設帳號,或留空 ""
+ "password": os.getenv("DB_PASS", ""), # 🌟 絕對機密:預設留空!
+ "host": os.getenv("DB_HOST", "127.0.0.1"),
+ "port": os.getenv("DB_PORT", "5432")
}
async def init_database(force_reset: bool = False):
@@ -1387,11 +1436,20 @@ def parse_question_mark_output(output: str) -> dict:
for line in output.splitlines():
line = line.strip()
- # 1. 過濾雜訊與 Echo (包含 prompt 和 ?)
+ # ==========================================
+ # 🛡️ 1. 終極雜訊與 Echo 過濾器
+ # ==========================================
if not line or line.startswith('%') or line.startswith('^'):
continue
- if '?' in line and ('#' in line or '>' in line):
+
+ # 攔截 Echo 的問號指令 (例如 "logging buffered ?")
+ if line.endswith('?'):
continue
+
+ # 攔截終端機 Prompt (例如 "admin@SERCOMM-COS-02(config)# ..." 或 "admin@SERCOMM-COS-02>")
+ if re.search(r"[a-zA-Z0-9_.@-]+\(.*\)#", line) or re.search(r"^[a-zA-Z0-9_.@-]+>", line):
+ continue
+ # ==========================================
# 2. 無差別收集所有有效行作為 Hint (保留最完整的說明給使用者看)
hint_lines.append(line)
@@ -4375,6 +4433,20 @@ function colorizeTerminalStream(text) {
// 4. 匯出 (export) 所有需要被外部呼叫的函數
export function initTerminal() {
+ const container = document.getElementById('terminal-container');
+
+ // ==========================================
+ // 🛡️ DOM 防火牆:攔截殘缺的假鍵盤事件
+ // ==========================================
+ container.addEventListener('keydown', (e) => {
+ // 如果這個事件物件沒有 getModifierState 函數 (代表它是外掛產生的假事件)
+ if (typeof e.getModifierState !== 'function') {
+ // 🛑 停止事件傳遞!不讓它流進 Xterm.js 裡
+ e.stopPropagation();
+ }
+ }, true); // 🌟 關鍵:傳入 true 啟用「捕獲階段 (Capture Phase)」,確保我們比 Xterm.js 更早拿到事件
+ // ==========================================
+
term = new Terminal({ cursorBlink: true, theme: { background: '#1e1e1e', foreground: '#e0e0e0' }, fontFamily: 'Courier New, monospace', fontSize: 15, scrollback: 100000 });
fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
@@ -4409,8 +4481,9 @@ export function connectWebSocket(resetCallback) {
btnLoadTask.style.backgroundColor = '#c0392b';
btnLoadTask.style.cursor = 'pointer';
}
- document.getElementById('wsStatus').textContent = `狀態:已連線`;
- document.getElementById('wsStatus').style.color = '#27ae60';
+ // 🌟 UX 升級:剛連上 WS 時,顯示「驗證中...」而不是直接顯示「已連線」
+ document.getElementById('wsStatus').textContent = `狀態:連線與驗證中...`;
+ document.getElementById('wsStatus').style.color = '#f39c12'; // 橘色
document.getElementById('btnConnect').style.display = 'none';
document.getElementById('btnDisconnect').style.display = 'inline-block';
document.getElementById('cmtsHost').disabled = true;
@@ -4432,6 +4505,12 @@ export function connectWebSocket(resetCallback) {
};
ws.onmessage = (event) => {
+ // 🌟 UX 升級:只要收到設備傳來的第一個字元,就代表 SSH 驗證成功,正式轉為綠色「已連線」
+ const statusEl = document.getElementById('wsStatus');
+ if (statusEl.textContent.includes('驗證中')) {
+ statusEl.textContent = `狀態:已連線`;
+ statusEl.style.color = '#27ae60'; // 綠色
+ }
term.write(colorizeTerminalStream(event.data));
};
@@ -4452,6 +4531,11 @@ export function connectWebSocket(resetCallback) {
document.getElementById('cmtsPass').disabled = false;
term.writeln('\r\n\x1b[31m--- Connection Closed ---\x1b[0m\r\n');
+ // 🌟 關鍵修復:攔截後端傳來的 4001 錯誤碼,彈出明確的警告視窗
+ if (event.code === 4001) {
+ alert(`❌ 連線失敗!\n請檢查目標設備 IP、帳號與密碼是否正確。\n\n系統訊息: ${event.reason}`);
+ }
+
// 🌟 新增:斷線時,強制觸發配置任務切換,藉此「關閉」SSE 監聽
const configTaskSelect = document.getElementById('configTask');
if (configTaskSelect) {
@@ -4986,6 +5070,14 @@ export async function startEditFolder(path, elementId) {
const detailsEl = document.getElementById(`details-${elementId}`);
if (detailsEl) detailsEl.open = true;
+ // ==========================================
+ // 🌟 完美修復:在抓取葉子節點前,強制將該資料夾底下的所有 HTML 預先渲染出來
+ // 這樣 querySelectorAll 就能一次抓到所有深層的欄位!
+ // ==========================================
+ if (typeof window.forceRenderFolderHTML === 'function') {
+ window.forceRenderFolderHTML(elementId);
+ }
+
const contentDiv = document.getElementById(`content-${elementId}`);
if (!contentDiv) return;
@@ -6406,8 +6498,8 @@ export function collapseAll(elementId) {
// ==========================================
// 🌟 核心函數:生成完整設備配置樹狀圖 (Lazy 版)
// ==========================================
-// 🌟 新增 forceExpand 參數,預設為 false
-export function buildTree(node, path = '', isParentCommandGroup = false, mode = 'running', forceExpand = false) {
+// 🌟 修改函數宣告,加入 renderAll 參數
+export function buildTree(node, path = '', isParentCommandGroup = false, mode = 'running', forceExpand = false, renderAll = false) {
const htmlParts = [];
if (!node || typeof node !== 'object') return htmlParts.join('');
@@ -6477,13 +6569,14 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode =
// 🚀 核心魔法:如果 forceExpand 為 true,直接在記憶體中遞迴生成子節點 HTML
const isOpenAttr = forceExpand ? 'open' : '';
- const isLoadedAttr = forceExpand ? 'true' : 'false';
+ const isLoadedAttr = (forceExpand || renderAll) ? 'true' : 'false';
const displayClosed = forceExpand ? 'none' : 'inline-block';
const displayOpen = forceExpand ? 'inline-block' : 'none';
let innerContent = '';
- if (forceExpand) {
- innerContent = buildTree(value, currentPath, isCommandGroup, mode, true);
+ if (forceExpand || renderAll) {
+ // 🌟 遞迴呼叫時,把 renderAll 傳遞下去
+ innerContent = buildTree(value, currentPath, isCommandGroup, mode, forceExpand, renderAll);
}
htmlParts.push(`
@@ -6755,6 +6848,25 @@ export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isPa
return htmlParts.join('');
}
+// ==========================================
+// 🌟 輔助函數:強制預先渲染資料夾 HTML (供編輯模式使用)
+// ==========================================
+window.forceRenderFolderHTML = function(elementId) {
+ const detailsEl = document.getElementById(`details-${elementId}`);
+ const contentDiv = document.getElementById(`content-${elementId}`);
+ const cache = folderDataCache[elementId];
+
+ if (detailsEl && contentDiv && cache && detailsEl.dataset.loaded !== 'true') {
+ const currentPath = detailsEl.dataset.path;
+ const isCommandGroup = detailsEl.dataset.isGroup === 'true';
+ const mode = detailsEl.dataset.mode;
+
+ // 重新渲染,傳入 renderAll = true (生成 HTML 但不展開)
+ contentDiv.innerHTML = buildTree(cache, currentPath, isCommandGroup, mode, false, true);
+ detailsEl.dataset.loaded = 'true';
+ }
+};
+
================================================================================
FILE: routers/backup.py
================================================================================
@@ -7190,12 +7302,19 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas
if ssh_task and not ssh_task.done():
ssh_task.cancel()
except Exception as e:
- error_msg = f"\r\n\x1b[31mSSH Connection Error: {str(e)}\x1b[0m\r\n"
+ err_str = str(e)
+ error_msg = f"\r\n\x1b[31mSSH Connection Error: {err_str}\x1b[0m\r\n"
try:
+ # 先把錯誤印在終端機畫面上
await websocket.send_text(error_msg)
+
+ # 🌟 關鍵修復:使用自訂斷線碼 4001,並附上錯誤原因 (WebSocket 規範 reason 最長 123 bytes)
+ safe_reason = err_str[:120] if err_str else "Authentication or Connection Failed"
+ await websocket.close(code=4001, reason=safe_reason)
except:
pass
logger.error("❌ [Connection Error] 建立連線或執行過程中發生錯誤", exc_info=True)
+ return # 🌟 提早結束,避免下方的 finally 再次執行 close 導致報錯
finally:
# 🌟 確保 process 與 conn 絕對被關閉,防止 Memory Leak
if process:
@@ -7792,8 +7911,8 @@ class GodModeRequest(BaseModel):
@router.post("/auth/god-mode", summary="驗證上帝模式進階密碼")
async def verify_god_mode(req: GodModeRequest):
- # 從環境變數讀取正確密碼,預設為 "admin999"
- expected_password = os.getenv("GOD_MODE_SECRET", "swpa@Serc0mm")
+ # 從環境變數讀取正確密碼,預設為 "19760107@Serc0mm"
+ expected_password = os.getenv("GOD_MODE_SECRET", "19760107@Serc0mm")
# 使用 compare_digest 防禦計時攻擊
if secrets.compare_digest(req.password, expected_password):
diff --git a/routers/terminal.py b/routers/terminal.py
index 9122d5c..3a09aba 100644
--- a/routers/terminal.py
+++ b/routers/terminal.py
@@ -80,12 +80,19 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas
if ssh_task and not ssh_task.done():
ssh_task.cancel()
except Exception as e:
- error_msg = f"\r\n\x1b[31mSSH Connection Error: {str(e)}\x1b[0m\r\n"
+ err_str = str(e)
+ error_msg = f"\r\n\x1b[31mSSH Connection Error: {err_str}\x1b[0m\r\n"
try:
+ # 先把錯誤印在終端機畫面上
await websocket.send_text(error_msg)
+
+ # 🌟 關鍵修復:使用自訂斷線碼 4001,並附上錯誤原因 (WebSocket 規範 reason 最長 123 bytes)
+ safe_reason = err_str[:120] if err_str else "Authentication or Connection Failed"
+ await websocket.close(code=4001, reason=safe_reason)
except:
pass
logger.error("❌ [Connection Error] 建立連線或執行過程中發生錯誤", exc_info=True)
+ return # 🌟 提早結束,避免下方的 finally 再次執行 close 導致報錯
finally:
# 🌟 確保 process 與 conn 絕對被關閉,防止 Memory Leak
if process:
diff --git a/static/terminal.js b/static/terminal.js
index 16b0df2..505ada5 100644
--- a/static/terminal.js
+++ b/static/terminal.js
@@ -79,8 +79,9 @@ export function connectWebSocket(resetCallback) {
btnLoadTask.style.backgroundColor = '#c0392b';
btnLoadTask.style.cursor = 'pointer';
}
- document.getElementById('wsStatus').textContent = `狀態:已連線`;
- document.getElementById('wsStatus').style.color = '#27ae60';
+ // 🌟 UX 升級:剛連上 WS 時,顯示「驗證中...」而不是直接顯示「已連線」
+ document.getElementById('wsStatus').textContent = `狀態:連線與驗證中...`;
+ document.getElementById('wsStatus').style.color = '#f39c12'; // 橘色
document.getElementById('btnConnect').style.display = 'none';
document.getElementById('btnDisconnect').style.display = 'inline-block';
document.getElementById('cmtsHost').disabled = true;
@@ -102,6 +103,12 @@ export function connectWebSocket(resetCallback) {
};
ws.onmessage = (event) => {
+ // 🌟 UX 升級:只要收到設備傳來的第一個字元,就代表 SSH 驗證成功,正式轉為綠色「已連線」
+ const statusEl = document.getElementById('wsStatus');
+ if (statusEl.textContent.includes('驗證中')) {
+ statusEl.textContent = `狀態:已連線`;
+ statusEl.style.color = '#27ae60'; // 綠色
+ }
term.write(colorizeTerminalStream(event.data));
};
@@ -122,6 +129,11 @@ export function connectWebSocket(resetCallback) {
document.getElementById('cmtsPass').disabled = false;
term.writeln('\r\n\x1b[31m--- Connection Closed ---\x1b[0m\r\n');
+ // 🌟 關鍵修復:攔截後端傳來的 4001 錯誤碼,彈出明確的警告視窗
+ if (event.code === 4001) {
+ alert(`❌ 連線失敗!\n請檢查目標設備 IP、帳號與密碼是否正確。\n\n系統訊息: ${event.reason}`);
+ }
+
// 🌟 新增:斷線時,強制觸發配置任務切換,藉此「關閉」SSE 監聽
const configTaskSelect = document.getElementById('configTask');
if (configTaskSelect) {