From 562625abb3168825470f4cea913d126244104da3 Mon Sep 17 00:00:00 2001 From: swpa Date: Thu, 30 Apr 2026 16:27:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=96=8B=E7=99=BC=E6=A8=B9=E7=8B=80?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=88=87=E7=B3=BB=E7=B5=B1=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E9=A0=81=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 77 +++++++++++-- routers/config.py | 123 +++++++++++++++++++- shared.py | 112 +++++++++++++++++++ static/app.js | 260 ++++++++++++++++++++++++++++++++----------- system_settings.json | 19 ++++ 5 files changed, 516 insertions(+), 75 deletions(-) create mode 100644 system_settings.json diff --git a/index.html b/index.html index 693ebd5..dbf111a 100644 --- a/index.html +++ b/index.html @@ -8,7 +8,7 @@ - + @@ -32,6 +32,8 @@ + + @@ -148,8 +150,8 @@ - - + +
@@ -157,17 +159,15 @@
- - - + -
- -
+ +

⚠️ MAC Domain 狀態感知配置精靈

@@ -180,7 +180,6 @@
+ + +
+ +
+
+
+ +

+ ⚙️ 全域系統設定 (God Mode Filters) +

+ +
+

+ 請勾選您希望在「完整設備配置樹狀圖」中 隱藏 的設定項目。
+ 💡 點擊下方按鈕載入設備實時配置,您可以深入展開並勾選任何層級的節點進行過濾。 +

+
+ + + + + + + +
+ + +
+
+
+
+ - + + diff --git a/routers/config.py b/routers/config.py index f405655..1545c8c 100644 --- a/routers/config.py +++ b/routers/config.py @@ -1,8 +1,11 @@ # --- routers/config.py --- +import re from fastapi import APIRouter, HTTPException from pydantic import BaseModel +from typing import List from netmiko import ConnectHandler -from shared import CMTS_DEVICE, cmts_config_lock +# 💡 確保從 shared 引入 deep_split_tree +from shared import CMTS_DEVICE, cmts_config_lock, parse_cli_to_tree, deep_split_tree, load_settings, save_settings router = APIRouter() @@ -35,3 +38,121 @@ async def execute_cmts_config(req: ConfigRequest): return {"status": "success", "data": output} except Exception as e: raise HTTPException(status_code=500, detail=f"CMTS Config Error: {str(e)}") + + +# ========================================== +# 💡 以下為 DS RF Port 動態樹狀查詢 API +# ========================================== + +@router.get("/cmts-ds-rf-port-list") +async def get_ds_rf_port_list(host: str, username: str, password: str = ""): + """獲取設備上所有的 DS RF Port 清單""" + try: + device = CMTS_DEVICE.copy() + device.update({'host': host, 'username': username, 'password': password}) + + net_connect = ConnectHandler(**device) + + # 使用 include 只抓取關鍵字,並把等待時間延長至 60 秒 + command = 'show running-config | include "cable ds-rf-port"' + raw_output = net_connect.send_command(command, read_timeout=60) + net_connect.disconnect() + + # 使用正規表達式擷取 port 號碼 (例如 62:0/0) + pattern = r"cable ds-rf-port\s+([0-9:/]+)" + ports = re.findall(pattern, raw_output) + unique_ports = sorted(list(set(ports))) + + return { + "status": "success", + "data": unique_ports, + "raw_output": raw_output + } + except Exception as e: + return {"status": "error", "message": str(e)} + + +@router.get("/cmts-ds-rf-port-config") +async def get_ds_rf_port_config(target: str, host: str, username: str, password: str = ""): + """獲取特定 DS RF Port 的配置,並轉換為樹狀 JSON""" + try: + device = CMTS_DEVICE.copy() + device.update({'host': host, 'username': username, 'password': password}) + + net_connect = ConnectHandler(**device) + command = f"show running-config interface cable ds-rf-port {target} | nomore" + raw_output = net_connect.send_command(command, read_timeout=60) + net_connect.disconnect() + + # 1. 初步解析縮排 + base_tree = parse_cli_to_tree(raw_output) + # 2. 💡 深層解析空白字元,建立完美樹狀結構 + perfect_tree = deep_split_tree(base_tree) + + return { + "status": "success", + "data": perfect_tree, # 回傳完美的樹狀結構 + "raw_cli": raw_output.strip() + } + except Exception as e: + return {"status": "error", "message": str(e)} + + +@router.get("/cmts-full-config") +async def get_full_config(host: str, username: str, password: str = "", skip_filter: bool = False): + """獲取整台 CMTS 的完整配置,並轉換為大型樹狀 JSON""" + try: + device = CMTS_DEVICE.copy() + device.update({'host': host, 'username': username, 'password': password}) + + net_connect = ConnectHandler(**device) + command = "show running-config | nomore" + raw_output = net_connect.send_command(command, read_timeout=60) + net_connect.disconnect() + + base_tree = parse_cli_to_tree(raw_output) + perfect_tree = deep_split_tree(base_tree) + + # ========================================== + # 💡 深度過濾攔截器:支援多層級路徑 (如 cable.mac-domain) + # ========================================== + if not skip_filter: + settings = load_settings() + hidden_keys = settings.get("hidden_keys", []) + + for path in hidden_keys: + keys = path.split('.') + current = perfect_tree + # 走到倒數第二層 + for k in keys[:-1]: + if isinstance(current, dict) and k in current: + current = current[k] + else: + current = None + break + # 刪除最後一層的目標節點 + if isinstance(current, dict) and keys[-1] in current: + current.pop(keys[-1], None) + # ========================================== + + return {"status": "success", "data": perfect_tree} + except Exception as e: + return {"status": "error", "message": str(e)} + +# ========================================== +# 💡 系統設定 API (System Settings) +# ========================================== +class SettingsRequest(BaseModel): + hidden_keys: list[str] + +@router.get("/settings/tree-filters") +async def get_tree_filters(): + """獲取目前的樹狀圖隱藏名單""" + settings = load_settings() + return {"status": "success", "data": settings["hidden_keys"]} + +@router.post("/settings/tree-filters") +async def update_tree_filters(req: SettingsRequest): + """更新樹狀圖隱藏名單並存檔""" + save_settings({"hidden_keys": req.hidden_keys}) + return {"status": "success", "message": "系統設定已更新"} diff --git a/shared.py b/shared.py index 48da544..95d4c61 100644 --- a/shared.py +++ b/shared.py @@ -1,5 +1,7 @@ # --- shared.py --- import asyncio +import json +import os DEBUG_MODE = False @@ -18,5 +20,115 @@ CMTS_DEVICE = { 'global_delay_factor': 2 } +def parse_cli_to_tree(cli_text: str) -> dict: + """ + 將 CMTS 的 CLI 配置文字,根據縮排轉換為巢狀 JSON (Dictionary) + """ + # 過濾掉空行與註解 (!) + lines = [line for line in cli_text.splitlines() if line.strip() and line.strip() != '!'] + + def parse_block(start_idx: int, base_indent: int): + block = {} + i = start_idx + while i < len(lines): + line = lines[i] + indent = len(line) - len(line.lstrip()) + + if indent < base_indent: + break + + content = line.strip() + + has_children = False + if i + 1 < len(lines): + next_indent = len(lines[i+1]) - len(lines[i+1].lstrip()) + if next_indent > base_indent: + has_children = True + + if has_children: + child_block, next_i = parse_block(i + 1, next_indent) + block[content] = child_block + i = next_i + else: + parts = content.split(maxsplit=1) + if len(parts) == 2: + block[parts[0]] = parts[1] + else: + block[parts[0]] = "" + i += 1 + + return block, i + + tree, _ = parse_block(0, 0) + return tree + +def deep_split_tree(data): + """將平坦的 CLI 字典,依照空白字元拆分成深層巢狀結構 (具備型態衝突保護)""" + if not isinstance(data, dict): + return data + + nested_tree = {} + for key, value in data.items(): + # 1. 遞迴處理子節點 + processed_value = deep_split_tree(value) + + # 2. 將當前的 key 依照空白拆分 + parts = str(key).strip().split() + if not parts: + continue + + # 3. 依序建立深層結構 + current = nested_tree + for i, part in enumerate(parts): + if i == len(parts) - 1: + # 抵達最後一個單字,準備塞入值 + if part not in current: + current[part] = processed_value + else: + # 💡 衝突保護:如果節點已存在,根據型態進行安全合併 + if isinstance(current[part], dict) and isinstance(processed_value, dict): + current[part].update(processed_value) + elif isinstance(current[part], dict) and not isinstance(processed_value, dict): + # 原本是資料夾,新來的是字串 -> 保留資料夾 + pass + elif not isinstance(current[part], dict) and isinstance(processed_value, dict): + # 原本是字串,新來的是資料夾 -> 升級成資料夾 + current[part] = processed_value + else: + # 兩者都是字串,直接覆蓋 + current[part] = processed_value + else: + # 建立中間的階層 + if part not in current: + current[part] = {} + elif not isinstance(current[part], dict): + # 💡 衝突保護:如果中間節點原本被當作字串,強制升級為字典(資料夾)以便容納子節點 + current[part] = {} + + current = current[part] + + return nested_tree + +# ========================================== +# 💡 系統設定管理 (System Settings Manager) +# ========================================== +SETTINGS_FILE = "system_settings.json" + +def load_settings(): + """讀取系統設定,若檔案不存在則回傳預設的黑名單""" + if os.path.exists(SETTINGS_FILE): + try: + with open(SETTINGS_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + pass + # 預設隱藏這些比較雜亂且不常修改的系統層級設定 + return {"hidden_keys": ["logging", "privilege", "aaa", "certificate", "line"]} + +def save_settings(settings_data): + """將系統設定寫入 JSON 檔案""" + with open(SETTINGS_FILE, "w", encoding="utf-8") as f: + json.dump(settings_data, f, indent=4, ensure_ascii=False) + # 全域非同步鎖:確保同一時間只有一人能執行設定寫入 cmts_config_lock = asyncio.Lock() diff --git a/static/app.js b/static/app.js index e1ce818..c3a23bc 100644 --- a/static/app.js +++ b/static/app.js @@ -5,7 +5,7 @@ // ========================================== let term, fitAddon, ws; let currentMacDomainData = null; -let isConnected = false; // 💡 新增:用來追蹤目前的連線狀態 +let isConnected = false; window.onload = initTerminal; window.onresize = () => { if (fitAddon) fitAddon.fit(); }; @@ -22,55 +22,56 @@ function switchTab(tabId, element) { if (element) element.classList.add('active'); if(tabId === 'cli-tab' && fitAddon) setTimeout(() => fitAddon.fit(), 50); - - // 💡 已經移除自動觸發 switchConfigTask(),避免切換頁籤時覆蓋使用者資料 } -// 💡 查詢頁籤專用的切換邏輯 function switchQueryTask() { - // 只隱藏 query-tab 裡面的表單 document.querySelectorAll('#query-tab .task-form').forEach(form => form.classList.remove('active')); const selectedTaskId = document.getElementById('queryTask').value; const targetForm = document.getElementById(selectedTaskId); if (targetForm) targetForm.classList.add('active'); } -// 💡 配置頁籤專用的切換邏輯 (現在只負責切換表單顯示,不自動抓資料) function switchConfigTask() { - // 只隱藏 config-tab 裡面的表單 - document.querySelectorAll('#config-tab .task-form').forEach(form => form.classList.remove('active')); + document.querySelectorAll('#config-tab .task-form').forEach(form => { + form.classList.remove('active'); + form.style.display = 'none'; + }); const selectedTaskId = document.getElementById('configTask').value; const targetForm = document.getElementById(selectedTaskId); - if (targetForm) targetForm.classList.add('active'); + if (targetForm) { + targetForm.classList.add('active'); + targetForm.style.display = 'block'; + } } -// 💡 新增的函數:由「🚀 載入任務」按鈕手動觸發 +// 💡 合併修復:統一處理「載入任務」的邏輯與防呆 function startConfigTask() { - // 💡 新增防呆:雙重檢查是否已連線 + // 防呆 1:檢查是否已連線 if (!isConnected) { - alert("請先成功連線至 CMTS 設備後,再執行載入任務!"); + alert("請先點擊畫面上方的「連線至 CMTS」成功後,再執行載入任務!"); return; } - // 1. 先確保對應的表單有顯示出來 + // 先確保對應的表單有顯示出來 switchConfigTask(); - // 2. 根據選擇的任務,決定要向設備抓取什麼資料 const selectedTaskId = document.getElementById('configTask').value; if (selectedTaskId === 'form-bonding-config') { - // 如果使用者正在編輯到一半,給予警告提示 + // 任務 A:MAC Domain 配置精靈 if (currentMacDomainData && !confirm("重新載入將會清除您目前未套用的設定,確定要繼續嗎?")) { - return; // 使用者取消,中斷執行 + return; } - - // 確定載入,重置畫面並向設備抓取 MAC Domain 清單 resetAllManagerData(); loadMacDomainList(); + + } else if (selectedTaskId === 'form-full-config') { + // 💡 修復:任務 B:完整設備配置樹狀圖 + // 當使用者點擊「載入任務」時,直接幫他觸發抓取樹狀圖的 API + fetchFullConfig(); } } -// 💡 新增函數:向後端請求 MAC Domain 清單並動態生成下拉選單 async function loadMacDomainList() { const connInfo = getGlobalConnectionInfo(); if (!connInfo) return; @@ -78,7 +79,6 @@ async function loadMacDomainList() { const mdSelect = document.getElementById('cfgMacDomain'); const statusSpan = document.getElementById('fetchStatus'); - // 顯示載入中狀態 mdSelect.innerHTML = ''; statusSpan.textContent = "正在自動抓取現有 MAC Domain 清單..."; statusSpan.style.color = "#f39c12"; @@ -89,16 +89,13 @@ async function loadMacDomainList() { const result = await response.json(); if (result.status === 'success' && result.data.length > 0) { - mdSelect.innerHTML = ''; // 清空預設選項 - - // 將後端抓到的陣列一一塞入下拉選單 + mdSelect.innerHTML = ''; result.data.forEach(md => { const option = document.createElement('option'); option.value = md; option.textContent = md; mdSelect.appendChild(option); }); - statusSpan.textContent = "✅ 清單抓取成功!請選擇目標並點擊讀取配置。"; statusSpan.style.color = "#27ae60"; } else { @@ -130,12 +127,9 @@ function toggleQueryInputs(mode) { } } -// 💡 新增函數:重置所有 Manager 頁籤的暫存資料與畫面 function resetAllManagerData() { - // 1. 清空記憶體中的設定資料 currentMacDomainData = null; - // 2. 隱藏 MAC Domain 配置表單與 CLI 預覽區塊 const configArea = document.getElementById('macDomainConfigArea'); if (configArea) configArea.style.display = 'none'; @@ -145,14 +139,12 @@ function resetAllManagerData() { previewEl.style.display = 'none'; } - // 3. 隱藏並禁用「正式套用設定」按鈕 const deployBtn = document.getElementById('btnDeployConfig'); if (deployBtn) { deployBtn.style.display = 'none'; deployBtn.disabled = true; } - // 4. 重置 MAC Domain 下拉選單與狀態文字 const mdSelect = document.getElementById('cfgMacDomain'); if (mdSelect) mdSelect.innerHTML = ''; @@ -162,7 +154,6 @@ function resetAllManagerData() { fetchStatus.style.color = "#7f8c8d"; } - // 5. (選擇性) 清空查詢表單的輸入框 document.getElementById('queryTargetCm').value = ''; document.getElementById('queryTargetRpd').value = ''; } @@ -211,7 +202,6 @@ function connectWebSocket() { const connInfo = getGlobalConnectionInfo(); if (!connInfo) return; - // 💡 關鍵修改 1:在發起新連線前,強制清空舊設備的所有殘留資料 resetAllManagerData(); term.writeln(`\x1b[33mConnecting to ${connInfo.host} via WebSocket...\x1b[0m`); @@ -221,12 +211,12 @@ function connectWebSocket() { ws = new WebSocket(wsUrl); ws.onopen = () => { - isConnected = true; // 💡 標記為已連線 + isConnected = true; const btnLoadTask = document.getElementById('btn-load-task'); if (btnLoadTask) { - btnLoadTask.disabled = false; // 解鎖按鈕功能 - btnLoadTask.style.backgroundColor = '#c0392b'; // 恢復原本的紅色 - btnLoadTask.style.cursor = 'pointer'; // 恢復正常滑鼠游標 + btnLoadTask.disabled = false; + btnLoadTask.style.backgroundColor = '#c0392b'; + btnLoadTask.style.cursor = 'pointer'; } document.getElementById('wsStatus').textContent = `狀態:已連線`; @@ -244,12 +234,12 @@ function connectWebSocket() { }; ws.onclose = () => { - isConnected = false; // 💡 標記為已斷線 + isConnected = false; const btnLoadTask = document.getElementById('btn-load-task'); if (btnLoadTask) { - btnLoadTask.disabled = true; // 鎖定按鈕功能 - btnLoadTask.style.backgroundColor = '#95a5a6'; // 變回反灰色 - btnLoadTask.style.cursor = 'not-allowed'; // 變回禁止游標 + btnLoadTask.disabled = true; + btnLoadTask.style.backgroundColor = '#95a5a6'; + btnLoadTask.style.cursor = 'not-allowed'; } document.getElementById('wsStatus').textContent = '狀態:已斷線'; @@ -261,7 +251,6 @@ function connectWebSocket() { document.getElementById('cmtsPass').disabled = false; term.writeln('\r\n\x1b[31m--- Connection Closed ---\x1b[0m\r\n'); - // 💡 關鍵修改 2:斷線時也自動重置畫面 resetAllManagerData(); }; } @@ -387,7 +376,6 @@ async function fetchMacDomainConfig() { function populateFormFromData() { const d = currentMacDomainData; - // 💡 正名工程:對齊後端 common_settings document.getElementById('g_ip_prov').value = d.common_settings['ip-provisioning-mode'] || 'dual-stack'; document.getElementById('g_diplexer').value = d.common_settings['diplexer-band-edge'] || 'enabled'; document.getElementById('g_bat31').value = d.common_settings['cm-battery-mode-31-support'] || 'disabled'; @@ -396,7 +384,6 @@ function populateFormFromData() { document.getElementById('g_ds_dyn').value = d.common_settings['ds-dynamic-bonding-group'] || 'disabled'; document.getElementById('g_us_dyn').value = d.common_settings['us-dynamic-bonding-group'] || 'disabled'; - // 💡 正名工程:對齊後端 basic_channel_sets document.getElementById('b_admin').value = d.basic_channel_sets['admin-state'] || 'down'; document.getElementById('b_ds_pri').value = d.basic_channel_sets['ds-primary-set'] || ''; document.getElementById('b_ds_non_pri').value = d.basic_channel_sets['ds-non-primary-set'] || ''; @@ -404,13 +391,11 @@ function populateFormFromData() { document.getElementById('b_ds_ofdm').value = d.basic_channel_sets['ds-ofdm-set'] || ''; document.getElementById('b_us_ofdma').value = d.basic_channel_sets['us-ofdma-set'] || ''; - // 💡 正名工程:對齊後端 static_ds_bonding_groups const dsSelect = document.getElementById('select_ds_group'); dsSelect.innerHTML = ''; Object.keys(d.static_ds_bonding_groups).forEach(grp => dsSelect.innerHTML += ``); if (Object.keys(d.static_ds_bonding_groups).length > 0) dsSelect.value = Object.keys(d.static_ds_bonding_groups)[0]; - // 💡 正名工程:對齊後端 static_us_bonding_groups const usSelect = document.getElementById('select_us_group'); usSelect.innerHTML = ''; Object.keys(d.static_us_bonding_groups).forEach(grp => usSelect.innerHTML += ``); @@ -421,20 +406,12 @@ function populateFormFromData() { loadUsGroupData(); } -// 💡 新增函數:放棄修改,還原回記憶體中的原始設備設定 function revertFormChanges() { - // 防呆:確保目前記憶體中有原始資料可以還原 if (!currentMacDomainData) return; + if (!confirm("確定要放棄目前的修改,還原回設備原始的設定值嗎?")) return; - // 加上確認對話框,避免使用者誤觸 - if (!confirm("確定要放棄目前的修改,還原回設備原始的設定值嗎?")) { - return; - } - - // 1. 重新利用記憶體中的原始 JSON 資料填入表單 populateFormFromData(); - // 2. 隱藏已經產生的 CLI 預覽與套用按鈕 (因為狀態已經改變) const previewEl = document.getElementById('cliPreviewContainer'); if (previewEl) { previewEl.textContent = ''; @@ -447,12 +424,10 @@ function revertFormChanges() { deployBtn.disabled = true; } - // 3. 給予使用者視覺回饋 const fetchStatus = document.getElementById('fetchStatus'); fetchStatus.textContent = "🔄 已還原為原始設定!"; fetchStatus.style.color = "#2980b9"; - // 3秒後把狀態文字改回綠色的讀取成功 setTimeout(() => { fetchStatus.textContent = "✅ 讀取成功!"; fetchStatus.style.color = "#27ae60"; @@ -477,7 +452,6 @@ function loadDsGroupData() { document.getElementById('ds_g_fdx').value = ''; } else { newGrpInput.style.display = 'none'; - // 💡 正名工程:對齊後端 static_ds_bonding_groups const data = currentMacDomainData.static_ds_bonding_groups[grp]; document.getElementById('ds_g_admin').value = data['admin-state'] || 'down'; document.getElementById('ds_g_down').value = data['down-channel-set'] || ''; @@ -497,7 +471,6 @@ function loadUsGroupData() { document.getElementById('us_g_fdx').value = ''; } else { newGrpInput.style.display = 'none'; - // 💡 正名工程:對齊後端 static_us_bonding_groups const data = currentMacDomainData.static_us_bonding_groups[grp]; document.getElementById('us_g_admin').value = data['admin-state'] || 'down'; document.getElementById('us_g_us').value = data['us-channel-set'] || ''; @@ -510,7 +483,6 @@ function generateMacDomainCLI() { const md = document.getElementById('cfgMacDomain').value.trim(); let cli = `! --- Harmonic MAC Domain Configuration SOP ---\n`; - // --- 區塊 1: MAC Domain Base --- cli += `! 1. [區塊] MAC Domain 全域與 Base 設定\n`; cli += `cable mac-domain ${md} admin-state down\ncommit\n`; @@ -533,7 +505,6 @@ function generateMacDomainCLI() { }); cli += `cable mac-domain ${md} admin-state ${document.getElementById('b_admin').value}\ncommit\n`; - // --- 區塊 2: DS Bonding Group --- if (document.getElementById('g_ds_dyn').value === 'disabled') { const isNewDs = document.getElementById('select_ds_group').value === '_new_'; let dsName = isNewDs ? document.getElementById('input_new_ds_group').value.trim() : document.getElementById('select_ds_group').value; @@ -550,7 +521,6 @@ function generateMacDomainCLI() { } } - // --- 區塊 3: US Bonding Group --- if (document.getElementById('g_us_dyn').value === 'disabled') { const isNewUs = document.getElementById('select_us_group').value === '_new_'; let usName = isNewUs ? document.getElementById('input_new_us_group').value.trim() : document.getElementById('select_us_group').value; @@ -614,7 +584,173 @@ async function executeBondingConfig() { } // ========================================== -// 6. Modal 彈出視窗控制 +// 💡 6. 完整設備配置樹狀圖邏輯 (從 HTML 移出) +// ========================================== +function buildTree(data) { + if (typeof data !== 'object' || data === null) { + return `${data}`; + } + + let html = '
'; + for (const key in data) { + const value = data[key]; + if (typeof value === 'object' && value !== null && Object.keys(value).length > 0) { + html += ` +
+ + 📁 ${key} + + ${buildTree(value)} +
+ `; + } else { + html += ` +
+ 📄 ${key}: ${value === null ? '' : value} +
+ `; + } + } + html += '
'; + return html; +} + +async function fetchFullConfig() { + const loadingMsg = document.getElementById('loading-message'); + const treeContainer = document.getElementById('tree-container'); + + const connInfo = getGlobalConnectionInfo(); + if (!connInfo) return; + + loadingMsg.style.display = 'block'; + treeContainer.innerHTML = ''; + + try { + const response = await fetch(`/api/v1/cmts-full-config?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}`); + const result = await response.json(); + + if (result.status === 'success') { + treeContainer.innerHTML = buildTree(result.data); + } else { + treeContainer.innerHTML = `
❌ 錯誤: ${result.message}
`; + } + } catch (error) { + treeContainer.innerHTML = `
❌ 連線失敗: ${error.message}
`; + } finally { + loadingMsg.style.display = 'none'; + } +} + +// ========================================== +// 7. 系統設定 (System Settings) 邏輯 +// ========================================== +let currentHiddenKeys = []; + +async function openSystemSettings() { + // 切換到此頁籤時,先默默讀取目前的隱藏名單 + try { + const response = await fetch('/api/v1/settings/tree-filters'); + const result = await response.json(); + if (result.status === 'success') { + currentHiddenKeys = result.data; + } + } catch (error) { + console.error("讀取設定失敗:", error); + } +} + +async function loadRealConfigForFilters() { + const connInfo = getGlobalConnectionInfo(); + if (!connInfo) { + alert("請先在畫面上方輸入設備連線資訊!"); + return; + } + + const loadingMsg = document.getElementById('filter-loading-msg'); + const container = document.getElementById('filter-checkboxes'); + + loadingMsg.style.display = 'inline-block'; + container.style.display = 'none'; + + try { + // 💡 加上 skip_filter=true,確保能抓到被隱藏的節點以便取消勾選 + const url = `/api/v1/cmts-full-config?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}&skip_filter=true`; + const response = await fetch(url); + const result = await response.json(); + + if (result.status === 'success') { + container.style.display = 'block'; + container.innerHTML = buildRealFilterTree(result.data, "", currentHiddenKeys); + } else { + container.style.display = 'block'; + container.innerHTML = `
❌ 錯誤: ${result.message}
`; + } + } catch (error) { + container.style.display = 'block'; + container.innerHTML = `
❌ 連線失敗: ${error.message}
`; + } finally { + loadingMsg.style.display = 'none'; + } +} + +function buildRealFilterTree(data, parentPath, hiddenKeys) { + if (typeof data !== 'object' || data === null) return ''; + + let html = `
`; + for (const key in data) { + const value = data[key]; + // 💡 組合絕對路徑,例如 "cable.mac-domain" + const currentPath = parentPath ? `${parentPath}.${key}` : key; + const isChecked = hiddenKeys.includes(currentPath) ? "checked" : ""; + + if (typeof value === 'object' && value !== null && Object.keys(value).length > 0) { + html += ` +
+ + + 📁 ${key} + + ${buildRealFilterTree(value, currentPath, hiddenKeys)} +
+ `; + } else { + html += ` +
+ + 📄 ${key}: ${value === null ? '' : value} +
+ `; + } + } + html += '
'; + return html; +} + +async function saveSystemSettings() { + const checkboxes = document.querySelectorAll('.filter-checkbox:checked'); + const selectedHiddenKeys = Array.from(checkboxes).map(cb => cb.value); + + try { + const response = await fetch('/api/v1/settings/tree-filters', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ hidden_keys: selectedHiddenKeys }) + }); + const result = await response.json(); + if (result.status === 'success') { + const statusText = document.getElementById('settings-status'); + statusText.style.display = 'inline-block'; + currentHiddenKeys = selectedHiddenKeys; // 更新本地狀態 + setTimeout(() => { statusText.style.display = 'none'; }, 3000); + } + } catch (error) { + console.error("儲存設定失敗:", error); + alert("儲存設定時發生錯誤!"); + } +} + +// ========================================== +// 8. Modal 彈出視窗控制 // ========================================== function openModal(targetInfo) { document.getElementById('outputModal').classList.add('active'); @@ -626,4 +762,4 @@ function closeModal(event) { if (event && event.target !== event.currentTarget && event.target.className !== 'modal-close') return; document.getElementById('outputModal').classList.remove('active'); document.body.style.overflow = ''; -} +} \ No newline at end of file diff --git a/system_settings.json b/system_settings.json new file mode 100644 index 0000000..abef151 --- /dev/null +++ b/system_settings.json @@ -0,0 +1,19 @@ +{ + "hidden_keys": [ + "alias", + "ssh", + "hostname", + "logging", + "no", + "ipdr", + "snmp-server", + "aaa", + "radius-server", + "privilege", + "cli", + "packetcable", + "system", + "network", + "clock" + ] +} \ No newline at end of file