開發樹狀配置與系統設定頁面

This commit is contained in:
swpa 2026-04-30 16:27:18 +08:00
parent f931c0f397
commit 07859300a6
5 changed files with 516 additions and 75 deletions

View File

@ -8,7 +8,7 @@
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
<!-- 引入我們剛剛獨立出來的 CSS 樣式表 -->
<!-- 引入獨立的 CSS 樣式表 -->
<link rel="stylesheet" href="/static/style.css" />
</head>
<body>
@ -32,6 +32,8 @@
<button class="tab-btn active" onclick="switchTab('cli-tab', this)">💻 True SSH Terminal</button>
<button class="tab-btn" onclick="switchTab('query-tab', this)">🔍 CMTS 狀態查詢</button>
<button class="tab-btn" onclick="switchTab('config-tab', this)">⚙️ CMTS 設備配置</button>
<!-- 💡 新增:系統設定頁籤 -->
<button class="tab-btn" onclick="switchTab('settings-tab', this); openSystemSettings();">⚙️ 系統設定</button>
</div>
<!-- Tab 1: True SSH Terminal -->
@ -148,8 +150,8 @@
</div>
</div>
</div>
</div> <!-- 💡 補上這個:關閉 query-tab 裡的 manager-container -->
</div> <!-- 💡 補上這個:關閉 query-tab 本身 -->
</div>
</div>
<!-- Tab 3: CMTS 設備配置 -->
<div id="config-tab" class="tab-content" style="overflow-y: auto; background-color: #fdfefe;">
@ -157,17 +159,15 @@
<div class="control-group" style="background: #ffffff; padding: 15px 25px; border-radius: 8px; margin-bottom: 0; box-shadow: 0 2px 4px rgba(0,0,0,0.02); border: 1px solid #e2e8f0;">
<label style="font-weight: bold; color: #c0392b; font-size: 16px; margin-right: 10px;">🛠️ 選擇配置任務:</label>
<!-- 💡 移除了 onchange改由右側按鈕手動觸發 -->
<select id="configTask" style="width: 400px; max-width: 100%; font-weight: bold; font-size: 15px; padding: 8px; background-color: #fcf3f2; border-color: #fadbd8;">
<select id="configTask" onchange="switchConfigTask()" style="width: 400px; max-width: 100%; font-weight: bold; font-size: 15px; padding: 8px; background-color: #fcf3f2; border-color: #fadbd8;">
<option value="form-bonding-config">⚠️ MAC Domain 狀態感知配置精靈</option>
<!-- 未來可以在這裡新增 modulation-profile, rpd 等選項 -->
<option value="form-full-config">🌳 完整設備配置樹狀圖</option>
</select>
<!-- 💡 新增的手動觸發按鈕 -->
<button id="btn-load-task" onclick="startConfigTask()" style="background-color: #95a5a6; color: white; margin-left: 10px; cursor: not-allowed;" disabled>🚀 載入任務</button>
</div>
<!-- 💡 表單 3MAC Domain 配置精靈 -->
<div id="form-bonding-config" class="task-form">
<!-- 表單 3MAC Domain 配置精靈 -->
<div id="form-bonding-config" class="task-form active">
<h3 style="margin-top: 0; font-size: 18px; color: #c0392b; border-bottom: 2px solid #ecf0f1; padding-bottom: 10px; margin-bottom: 20px;">⚠️ MAC Domain 狀態感知配置精靈</h3>
<div class="control-group" style="background: #fdf2e9; padding: 15px; border-radius: 6px; border: 1px solid #fadbd8;">
@ -180,7 +180,6 @@
</div>
<div id="macDomainConfigArea" style="display: none; margin-top: 20px;">
<!-- Common Settings -->
<h4 style="color: #2980b9; border-left: 4px solid #2980b9; padding-left: 8px;">Common Settings</h4>
<div class="form-grid">
@ -240,7 +239,6 @@
<div id="cliPreviewContainer" style="display: none; background: #2c3e50; color: #ecf0f1; padding: 15px; border-radius: 6px; margin-top: 30px; margin-bottom: 20px; font-family: monospace; white-space: pre-wrap;"></div>
<div class="form-actions">
<!-- 💡 新增的復原按鈕 (使用灰色系,避免與主要動作搶視覺焦點) -->
<button id="btnRevert" onclick="revertFormChanges()" style="background-color: #7f8c8d; color: white; margin-right: 15px;">
🔄 復原重填
</button>
@ -250,6 +248,60 @@
</div>
</div>
<!-- 表單 4完整設備配置樹狀圖 -->
<div id="form-full-config" class="task-form" style="display: none;">
<h3 style="margin-top: 0; font-size: 18px; color: #27ae60; border-bottom: 2px solid #ecf0f1; padding-bottom: 10px; margin-bottom: 20px;">🌳 完整設備配置樹狀圖</h3>
<div class="control-group" style="background: #eafaf1; padding: 15px; border-radius: 6px; border: 1px solid #a9dfbf;">
<label style="font-weight: bold; color: #1e8449;">點擊下方按鈕,將抓取整台設備的完整配置並轉為樹狀結構:</label>
<br>
<button onclick="fetchFullConfig()" style="background-color: #27ae60; color: white; margin-top: 10px; padding: 8px 16px;">📥 載入完整配置</button>
<div id="loading-message" style="display: none; color: #d35400; margin-top: 15px; font-weight: bold;">
⏳ 正在從設備抓取完整配置,可能需要 30~60 秒,請耐心稍候...
</div>
</div>
<div id="tree-container" style="margin-top: 20px; border: 1px solid #bdc3c7; padding: 15px; border-radius: 5px; background-color: #ffffff; overflow-x: auto; max-height: 600px; overflow-y: auto;">
<span style="color: #7f8c8d;">尚未載入資料。請點擊上方按鈕開始。</span>
</div>
</div>
</div>
</div>
<!-- 💡 Tab 4: 系統設定 (System Settings) -->
<div id="settings-tab" class="tab-content" style="overflow-y: auto; background-color: #fdfefe; text-align: left;">
<div class="manager-container" style="display: block; max-width: 100%;">
<div class="control-group" style="background: #ffffff; padding: 25px 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); border: 1px solid #e2e8f0; text-align: left; display: block;">
<h3 style="margin-top: 0; font-size: 18px; color: #2c3e50; border-bottom: 2px solid #ecf0f1; padding-bottom: 10px; margin-bottom: 20px;">
⚙️ 全域系統設定 (God Mode Filters)
</h3>
<div style="background: #f8f9fa; border-left: 4px solid #3498db; padding: 12px 15px; border-radius: 4px; margin-bottom: 20px;">
<p style="color: #2c3e50; font-size: 15px; margin: 0;">
請勾選您希望在「完整設備配置樹狀圖」中 <b>隱藏</b> 的設定項目。<br>
<span style="color: #7f8c8d; font-size: 14px;">💡 點擊下方按鈕載入設備實時配置,您可以深入展開並勾選任何層級的節點進行過濾。</span>
</p>
</div>
<button onclick="loadRealConfigForFilters()" style="background-color: #3498db; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; margin-bottom: 15px;">
📥 載入設備配置以設定過濾
</button>
<span id="filter-loading-msg" style="display: none; color: #e67e22; margin-left: 10px; font-weight: bold;">⏳ 正在抓取配置,請稍候...</span>
<!-- 樹狀 Checkbox 容器 -->
<div id="filter-checkboxes" style="margin: 15px 0; border: 1px solid #bdc3c7; padding: 15px; border-radius: 5px; background-color: #ffffff; overflow-x: auto; max-height: 500px; overflow-y: auto; display: none;">
</div>
<div style="margin-top: 20px; border-top: 1px solid #ecf0f1; padding-top: 20px;">
<button onclick="saveSystemSettings()" style="background-color: #27ae60; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; font-size: 15px; font-weight: bold;">
💾 儲存並套用設定
</button>
<span id="settings-status" style="margin-left: 15px; color: #27ae60; font-weight: bold; display: none;">✅ 設定已成功儲存!</span>
</div>
</div>
</div>
</div>
@ -269,7 +321,8 @@
</div>
</div>
<!-- 引入我們剛剛獨立出來的 JavaScript 邏輯 -->
<!-- 引入獨立的 JavaScript 邏輯 -->
<script src="/static/app.js"></script>
</body>
</html>

View File

@ -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": "系統設定已更新"}

112
shared.py
View File

@ -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()

View File

@ -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') {
// 如果使用者正在編輯到一半,給予警告提示
// 任務 AMAC 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 = '<option value="">⏳ 查詢設備中...</option>';
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 = '<option value="">請先點擊上方載入任務...</option>';
@ -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 = '<option value="_new_">[ 新增 DS Group]</option>';
Object.keys(d.static_ds_bonding_groups).forEach(grp => dsSelect.innerHTML += `<option value="${grp}">${grp}</option>`);
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 = '<option value="_new_">[ 新增 US Group]</option>';
Object.keys(d.static_us_bonding_groups).forEach(grp => usSelect.innerHTML += `<option value="${grp}">${grp}</option>`);
@ -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 `<span>${data}</span>`;
}
let html = '<div style="margin-left: 20px;">';
for (const key in data) {
const value = data[key];
if (typeof value === 'object' && value !== null && Object.keys(value).length > 0) {
html += `
<details>
<summary style="cursor: pointer; font-weight: bold; color: #2c3e50; margin-top: 5px; padding: 2px 0;">
📁 ${key}
</summary>
${buildTree(value)}
</details>
`;
} else {
html += `
<div style="padding: 2px 0; color: #34495e; margin-left: 20px; border-left: 1px solid #ecf0f1; padding-left: 10px;">
📄 <b>${key}</b>: <span style="color: #16a085;">${value === null ? '' : value}</span>
</div>
`;
}
}
html += '</div>';
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 = `<div style="color: #c0392b; font-weight: bold;">❌ 錯誤: ${result.message}</div>`;
}
} catch (error) {
treeContainer.innerHTML = `<div style="color: #c0392b; font-weight: bold;">❌ 連線失敗: ${error.message}</div>`;
} 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 = `<div style="color: #c0392b; font-weight: bold;">❌ 錯誤: ${result.message}</div>`;
}
} catch (error) {
container.style.display = 'block';
container.innerHTML = `<div style="color: #c0392b; font-weight: bold;">❌ 連線失敗: ${error.message}</div>`;
} finally {
loadingMsg.style.display = 'none';
}
}
function buildRealFilterTree(data, parentPath, hiddenKeys) {
if (typeof data !== 'object' || data === null) return '';
let html = `<div style="margin-left: ${parentPath ? '25px' : '0'};">`;
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 += `
<details>
<summary style="cursor: pointer; font-weight: bold; color: #2c3e50; margin-top: 5px; padding: 4px 0; display: flex; align-items: center; list-style: none;">
<input type="checkbox" value="${currentPath}" class="filter-checkbox" style="margin-right: 8px; cursor: pointer; width: 16px; height: 16px; accent-color: #e74c3c;" ${isChecked}>
<span style="margin-right: 5px;">📁</span> ${key}
</summary>
${buildRealFilterTree(value, currentPath, hiddenKeys)}
</details>
`;
} else {
html += `
<div style="padding: 4px 0; color: #34495e; margin-left: 20px; border-left: 1px solid #ecf0f1; padding-left: 10px; display: flex; align-items: center;">
<input type="checkbox" value="${currentPath}" class="filter-checkbox" style="margin-right: 8px; cursor: pointer; width: 16px; height: 16px; accent-color: #e74c3c;" ${isChecked}>
<span style="margin-right: 5px;">📄</span> <b>${key}</b>: <span style="color: #16a085; margin-left: 5px;">${value === null ? '' : value}</span>
</div>
`;
}
}
html += '</div>';
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');

19
system_settings.json Normal file
View File

@ -0,0 +1,19 @@
{
"hidden_keys": [
"alias",
"ssh",
"hostname",
"logging",
"no",
"ipdr",
"snmp-server",
"aaa",
"radius-server",
"privilege",
"cli",
"packetcable",
"system",
"network",
"clock"
]
}