diff --git a/cmts_leaf_options_cache.json b/cmts_leaf_options_cache.json index fd4f67b..e65c821 100644 --- a/cmts_leaf_options_cache.json +++ b/cmts_leaf_options_cache.json @@ -99094,5 +99094,94 @@ "type": "string_or_number", "current_value": "10", "updated_at": 1778582997 + }, + "logging evt 1024 description": { + "hint": "Description: Enter a meaningful description\nPossible completions:\n[Cold Start]", + "options": [], + "type": "string_or_number", + "current_value": "Cold Start", + "updated_at": 1778643770 + }, + "logging evt 1024 name": { + "hint": "Description: Short Description\nPossible completions:\n[cold-start]", + "options": [], + "type": "string_or_number", + "current_value": "cold-start", + "updated_at": 1778644476 + }, + "logging evt 1024 priority": { + "hint": "Description: Event Priority\nPossible completions:\n[notice] alert critical debug emergency error information notice warning", + "options": [ + "alert", + "critical", + "debug", + "emergency", + "error", + "information", + "notice", + "warning" + ], + "type": "list", + "current_value": "notice", + "updated_at": 1778644476 + }, + "logging evt 1024 active": { + "hint": "Description: Enable/Disable the event\nPossible completions:\n[enabled] disabled enabled", + "options": [ + "disabled", + "enabled" + ], + "type": "list", + "current_value": "enabled", + "updated_at": 1778644476 + }, + "logging evt 1024 destination": { + "hint": "Description: Event destination \nPossible completions:\n[]", + "options": [], + "type": "string_or_number", + "current_value": null, + "updated_at": 1778644476 + }, + "radius-server timeout": { + "hint": "Possible completions:\n[5]", + "options": [], + "type": "string_or_number", + "current_value": "5", + "updated_at": 1778644492 + }, + "radius-server retransmit": { + "hint": "Possible completions:\n[3]", + "options": [], + "type": "string_or_number", + "current_value": "3", + "updated_at": 1778644492 + }, + "snmp-server view all": { + "hint": "Possible completions:\n 1 ", + "options": [], + "type": "string_or_number", + "current_value": null, + "updated_at": 1778654743 + }, + "snmp-server group": { + "hint": "Possible completions:\n private", + "options": [], + "type": "string_or_number", + "current_value": null, + "updated_at": 1778654796 + }, + "aaa": { + "hint": "Possible completions:\naccounting\nauthentication\nauthorization\nnew-model Set/Unset to enable/disable AAA", + "options": [], + "type": "string_or_number", + "current_value": null, + "updated_at": 1778654851 + }, + "cli prompt1": { + "hint": "Description: Prompt for operational mode\nPossible completions:\n[\\u@SERCOMM-COS-02> ]", + "options": [], + "type": "string_or_number", + "current_value": "\\u@SERCOMM-COS-02>", + "updated_at": 1778654885 } } \ No newline at end of file diff --git a/static/api.js b/static/api.js new file mode 100644 index 0000000..d952b7b --- /dev/null +++ b/static/api.js @@ -0,0 +1,121 @@ +// --- static/api.js --- + +// ========================================== +// 1. 鎖定管理 (Lock Management) +// ========================================== +export async function apiAcquireLock(path, userId, username) { + const response = await fetch('/api/v1/locks/acquire', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path, user_id: userId, username }) + }); + return { status: response.status, data: await response.json() }; +} + +export async function apiReleaseLock(path, userId) { + return fetch('/api/v1/locks/release', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path, user_id: userId }) + }); +} + +export async function apiSendHeartbeat(path, userId) { + return fetch('/api/v1/locks/heartbeat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path, user_id: userId }) + }); +} + +// ========================================== +// 2. 樹狀圖與選項快取 (Tree & Options) +// ========================================== +export async function apiGetFullConfig(host, username, password, skipFilter = false) { + let url = `/api/v1/cmts-full-config?host=${encodeURIComponent(host)}&username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`; + if (skipFilter) url += '&skip_filter=true'; + const response = await fetch(url); + return response.json(); +} + +export async function apiGetLeafOptions() { + const response = await fetch('/api/v1/cmts-leaf-options'); + return response.json(); +} + +export async function apiSyncLeafOptions(paths) { + return fetch('/api/v1/cmts-leaf-options/sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ leaf_paths: paths }) + }); +} + +export async function apiClearCache(paths) { + const response = await fetch('/api/v1/clear_cache', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(paths) + }); + if (!response.ok) throw new Error(`伺服器回應錯誤: ${response.status}`); + return response.json(); +} + +// ========================================== +// 3. 指令生成與執行 (CLI Generation & Execution) +// ========================================== +export async function apiGenerateCli(diffs, interfaces) { + const response = await fetch('/api/v1/cmts-generate-cli', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ diffs, interfaces_with_admin_state: interfaces }) + }); + return response.json(); +} + +export async function apiExecuteConfig(script, host, username, password) { + const response = await fetch('/api/v1/cmts-config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ script, host, username, password }) + }); + return response.json(); +} + +// ========================================== +// 4. 系統設定與過濾器 (System Settings) +// ========================================== +export async function apiGetTreeFilters() { + const response = await fetch('/api/v1/settings/tree-filters'); + return response.json(); +} + +export async function apiSaveTreeFilters(hiddenKeys) { + const response = await fetch('/api/v1/settings/tree-filters', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ hidden_keys: hiddenKeys }) + }); + return response.json(); +} + +// ========================================== +// 5. MAC Domain 與綜合查詢 (MAC Domain & Query) +// ========================================== +export async function apiGetMacDomainList(host, username, password) { + const url = `/api/v1/cmts-mac-domain-list?host=${encodeURIComponent(host)}&username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`; + const response = await fetch(url); + return response.json(); +} + +export async function apiGetMacDomainConfig(target, host, username, password) { + const url = `/api/v1/cmts-mac-domain-config?target=${encodeURIComponent(target)}&host=${encodeURIComponent(host)}&username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`; + const response = await fetch(url); + return response.json(); +} + +export async function apiExecuteQuery(queryType, target, host, username, password) { + const url = `/api/v1/cmts-query?query_type=${queryType}&target=${encodeURIComponent(target)}&host=${encodeURIComponent(host)}&username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`; + const response = await fetch(url); + return response.json(); +} diff --git a/static/app.js b/static/app.js index 030634b..78eae51 100644 --- a/static/app.js +++ b/static/app.js @@ -1,95 +1,57 @@ /* --- static/app.js --- */ -// 1. 從 terminal.js 引入我們需要的功能 -import { initTerminal, connectWebSocket, disconnectWebSocket, injectCommand, downloadTerminal } from './terminal.js'; +// ============================================================================ +// 📦 模組匯入區 (Imports) +// ============================================================================ -// 2. 將這些功能綁定到 window 上,這樣 HTML 裡的 onclick 才找得到它們! -window.connectWebSocket = () => connectWebSocket(resetAllManagerData); -window.disconnectWebSocket = disconnectWebSocket; -window.injectCommand = injectCommand; -window.downloadTerminal = downloadTerminal; +// 1. 終端機模組 (Terminal) +import { initTerminal, connectWebSocket, disconnectWebSocket, injectCommand, downloadTerminal, isConnected } from './terminal.js'; -// ========================================== -// 1. 全域變數與初始化 -// ========================================== -let currentMacDomainData = null; +// 2. API 網路請求模組 (API) +import { + apiGetFullConfig, apiClearCache, apiExecuteQuery, + apiGetTreeFilters, apiSaveTreeFilters +} from './api.js'; -// 💡 新增:追蹤當前編輯狀態 -let currentEditPath = null; -let currentEditElementId = null; -let currentEditIsSuccess = false; -let currentEditType = null; // 💡 新增:用來記錄是 'folder' 還是 'leaf' +// 3. 共用工具模組 (Utils) +import { getGlobalConnectionInfo } from './utils.js'; + +// 4. 樹狀圖 UI 模組 (Tree UI) +import { buildTree, buildRealFilterTree, expandAll, collapseAll } from './tree-ui.js'; + +// 5. 編輯模式與鎖定模組 (Edit Mode) +import { + releaseAllLocks, startEditFolder, startEditLeaf, + cancelEditFolder, cancelEditLeaf, previewFolderCLI, previewLeafCLI, + hideSideCLI, executeSideCLI +} from './edit-mode.js'; + +// 6. MAC Domain 配置模組 (MAC Domain) +import { + hasMacDomainData, resetMacDomainData, loadMacDomainList, fetchMacDomainConfig, + populateFormFromData, revertFormChanges, toggleGroupVisibility, + loadDsGroupData, loadUsGroupData, generateMacDomainCLI, executeBondingConfig +} from './mac-domain.js'; + + +// ============================================================================ +// 🌟 1. 全域生命週期與閒置偵測 (Lifecycle & Idle Detection) +// ============================================================================ -// 💤 全域閒置偵測 let idleTimer; const IDLE_TIMEOUT = 300000; // 5 分鐘 (毫秒) + +// 頁面載入與縮放初始化 window.onload = () => { initTerminal(); resetIdleTimer(); // 頁面載入時啟動閒置計時器 }; -window.onresize = () => { if (fitAddon) fitAddon.fit(); }; +window.onresize = () => { if (typeof fitAddon !== 'undefined' && fitAddon) fitAddon.fit(); }; -// ========================================== -// 💡 全域狀態與鎖定管理 (Lock Management) -// ========================================== -const SESSION_USER_ID = crypto.randomUUID ? crypto.randomUUID() : 'user-' + Math.random().toString(36).substr(2, 9); -const ACTIVE_HEARTBEATS = {}; - -// 💤 閒置超時,自動釋放所有鎖 -async function releaseAllLocks() { - const pathsToRelease = Object.keys(ACTIVE_HEARTBEATS); - if (pathsToRelease.length === 0) return; // 如果沒有鎖,就不用執行 - - // 建立一個 Promise 陣列,來平行處理所有釋放請求 - const releasePromises = pathsToRelease.map(path => { - // 1. 清除心跳計時器 - clearInterval(ACTIVE_HEARTBEATS[path]); - delete ACTIVE_HEARTBEATS[path]; - - // 2. 發送釋放請求 - return fetch('/api/v1/locks/release', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ path: path, user_id: SESSION_USER_ID }) - }).catch(err => console.error(`釋放鎖定 ${path} 失敗:`, err)); - }); - - // 3. 等待所有釋放請求完成 - await Promise.all(releasePromises); - - // 4. 提示並重整頁面 - alert("您已閒置超過 5 分鐘,系統已自動釋放編輯鎖定並重新整理頁面。"); - location.reload(); -} - -// 💓 發送心跳,告訴後端「我還在編輯」,請延長鎖定時間 -async function sendHeartbeat(path) { - try { - const response = await fetch('/api/v1/locks/heartbeat', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ path: path, user_id: SESSION_USER_ID }) - }); - - if (!response.ok) { - console.warn(`心跳發送失敗 (${path}): 伺服器回傳 ${response.status}`); - // 如果回傳 404,代表後端的鎖已經被清掉了,停止發送心跳 - if (response.status === 404) { - clearInterval(ACTIVE_HEARTBEATS[path]); - delete ACTIVE_HEARTBEATS[path]; - } - } else { - console.log(`💓 心跳已送出,鎖定已延長 (${path})`); // 測試用,確認沒問題後可刪除這行 - } - } catch (error) { - console.error(`心跳請求發生錯誤 (${path}):`, error); - } -} - -// 實作閒置計時器重置邏輯 +// 閒置計時器重置邏輯 (超時將自動釋放所有編輯鎖定) function resetIdleTimer() { clearTimeout(idleTimer); - idleTimer = setTimeout(releaseAllLocks, IDLE_TIMEOUT); // IDLE_TIMEOUT 是 300000 + idleTimer = setTimeout(releaseAllLocks, IDLE_TIMEOUT); } // 監聽使用者的互動事件,只要有動作就重置計時器 @@ -97,922 +59,12 @@ window.addEventListener('mousemove', resetIdleTimer); window.addEventListener('keydown', resetIdleTimer); window.addEventListener('click', resetIdleTimer); -// ========================================== -// 🌟 輔助函數:全部展開與全部收合 -// ========================================== -function expandAll(elementId) { - const details = document.getElementById(`details-${elementId}`); - if (details) { - details.open = true; // 展開自己 - const subDetails = details.querySelectorAll('details'); - subDetails.forEach(d => d.open = true); // 展開所有子項目 - } -} -function collapseAll(elementId) { - const details = document.getElementById(`details-${elementId}`); - if (details) { - const subDetails = details.querySelectorAll('details'); - subDetails.forEach(d => d.open = false); // 收合所有子項目 - details.open = false; // 收合自己 - } -} +// ============================================================================ +// 🌟 2. 頁籤切換與 UI 狀態控制 (Tabs & UI State) +// ============================================================================ -// 🎨 輔助函數:解析終端機輸出,僅將錯誤/警告行標紅 -function highlightTerminalOutput(text) { - if (!text) return ''; - - // 將多行字串拆分成陣列,逐行檢查 - const lines = text.split('\n'); - const formattedLines = lines.map(line => { - // 偵測常見的錯誤與警告關鍵字 (不分大小寫) - // 您可以隨時在這裡擴充設備專屬的錯誤代碼,例如 '% Invalid' - if (line.match(/(Aborted|Error|Warning|Failed|% Invalid|% Unknown|Bad command)/i)) { - return `${line}`; - } - return line; // 沒有關鍵字的行,維持預設顏色 (白色) - }); - - return formattedLines.join('\n'); -} - -// ========================================== -// 💡 資料夾層級的編輯模式邏輯 (支援區塊級預先抓取) -// ========================================== -async function startEditFolder(path, elementId) { - const connInfo = getGlobalConnectionInfo(); - const username = connInfo ? connInfo.user : 'admin'; - - try { - const response = await fetch('/api/v1/locks/acquire', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ path: path, user_id: SESSION_USER_ID, username: username }) - }); - - const result = await response.json(); - - if (response.status === 409) { - alert(`⚠️ ${result.detail}`); - return; - } - - if (result.status === 'success') { - ACTIVE_HEARTBEATS[path] = setInterval(() => sendHeartbeat(path), 15000); - - document.getElementById(`edit-btn-${elementId}`).style.display = 'none'; - document.getElementById(`actions-${elementId}`).style.display = 'inline-block'; - document.getElementById(`details-${elementId}`).open = true; - - const contentDiv = document.getElementById(`content-${elementId}`); - const leafContainers = contentDiv.querySelectorAll('.leaf-container'); - - // 🌟 1. 先取得目前的快取資料 - let optData = {}; - try { - const optRes = await fetch('/api/v1/cmts-leaf-options'); - optData = await optRes.json(); - } catch (e) { - console.warn("無法取得選項快取", e); - } - - const pathsToSync = []; - const currentTime = Math.floor(Date.now() / 1000); - const CACHE_TTL = 86400 * 7; - - // 🌟 2. 歷遍所有欄位,渲染 UI 並收集缺少的路徑 - leafContainers.forEach(container => { - const origVal = container.getAttribute('data-original'); - const rawPath = container.getAttribute('data-path'); - // 🌟 轉換為空白格式後,全域拔除路徑中所有的虛擬索引 [0], [1] - let cacheKey = rawPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, ''); - - // 💡 攔截 no 指令:將查詢路徑改為實際指令 - if (rawPath.endsWith('::no')) { - cacheKey = cacheKey.replace(/ no$/, '') + ' ' + origVal; - } - - let leafCache = optData[cacheKey]; - - // 判斷是否需要排程更新 (沒快取,或是快取過期) - if (!leafCache || (currentTime - leafCache.updated_at >= CACHE_TTL)) { - pathsToSync.push(cacheKey); - } - - // 渲染 UI (加入提示與下拉選單邏輯) - let inputHtml = ""; - let hintAttr = ""; - let hintIcon = ""; - - if (leafCache && leafCache.hint) { - const safeHint = leafCache.hint.replace(/"/g, '"'); - hintAttr = `title="${safeHint}"`; - hintIcon = ``; - } - - if (leafCache && leafCache.options && leafCache.options.length > 0) { - let options = leafCache.options; - if (origVal && !options.includes(origVal)) { - options = [origVal, ...options]; - } - - inputHtml = `${hintIcon}`; - } else { - // 退回純文字輸入 - inputHtml = `${hintIcon}`; - } - - container.innerHTML = inputHtml; - }); - - // 🌟 3. 觸發背景抓取:將缺少的路徑一次性送給後端 - if (pathsToSync.length > 0) { - console.log(`🚀 將 ${pathsToSync.length} 個欄位送入背景排程抓取...`); - fetch('/api/v1/cmts-leaf-options/sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ leaf_paths: pathsToSync }) - }); - } - } - } catch (error) { - alert("❌ 無法連線到鎖定伺服器:" + error.message); - } -} - -// 🔓 鎖定並開始編輯單一項目 (加入智慧等待機制) -async function startEditLeaf(path, elementId) { - const connInfo = getGlobalConnectionInfo(); - const username = connInfo ? connInfo.user : 'admin'; - - try { - const response = await fetch('/api/v1/locks/acquire', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ path: path, user_id: SESSION_USER_ID, username: username }) - }); - - const result = await response.json(); - if (response.status === 409) return alert(`⚠️ ${result.detail}`); - - if (result.status === 'success') { - ACTIVE_HEARTBEATS[path] = setInterval(() => sendHeartbeat(path), 15000); - - document.getElementById(`edit-btn-${elementId}`).style.display = 'none'; - document.getElementById(`actions-${elementId}`).style.display = 'inline-block'; - - const container = document.getElementById(`container-${elementId}`); - const origVal = container.getAttribute('data-original'); - - // 💡 顯示載入中提示 - container.innerHTML = `⏳ 設備連線與載入選項中...`; - - try { - let optRes = await fetch('/api/v1/cmts-leaf-options'); - let optData = await optRes.json(); - - // 🌟 轉換為空白格式後,全域拔除路徑中所有的虛擬索引 [0], [1] - let cacheKey = path.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, ''); - - // 💡 攔截 no 指令:將查詢路徑改為實際指令 - if (path.endsWith('::no')) { - cacheKey = cacheKey.replace(/ no$/, '') + ' ' + origVal; - } - - let leafCache = optData[cacheKey]; - - const currentTime = Math.floor(Date.now() / 1000); - const CACHE_TTL = 86400 * 7; - - // 💡 如果沒快取,觸發背景抓取,並啟動「智慧等待」 - if (!leafCache || (currentTime - leafCache.updated_at >= CACHE_TTL)) { - // 1. 呼叫背景任務 - fetch('/api/v1/cmts-leaf-options/sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ leaf_paths: [cacheKey] }) - }); - - // 2. 每秒檢查一次,最多等 10 秒 - let found = false; - for (let i = 0; i < 10; i++) { - await new Promise(resolve => setTimeout(resolve, 1000)); // 暫停 1 秒 - - optRes = await fetch('/api/v1/cmts-leaf-options'); - optData = await optRes.json(); - leafCache = optData[cacheKey]; - - if (leafCache && leafCache.options && leafCache.options.length > 0) { - found = true; - break; // 找到了!跳出等待迴圈 - } - } - - if (!found) { - console.warn("⏳ 等待選項超時,退回純文字模式"); - } - } - - let inputHtml = ""; - let hintAttr = ""; - let hintIcon = ""; - - // 💡 處理 Hint 屬性與提示圖示 - if (leafCache && leafCache.hint) { - // 替換雙引號避免 HTML 屬性破裂 - const safeHint = leafCache.hint.replace(/"/g, '"'); - hintAttr = `title="${safeHint}"`; - // 加上一個小問號圖示,視覺上引導使用者懸停 - hintIcon = ``; - } - - // 🌟 渲染精緻的下拉選單 - if (leafCache && leafCache.options && leafCache.options.length > 0) { - let options = leafCache.options; - if (origVal && !options.includes(origVal)) { - options = [origVal, ...options]; - } - - inputHtml = `${hintIcon}`; - } else { - // ❌ 若為 list 且無選項,退回純文字輸入,但依然綁定提示 - inputHtml = `${hintIcon}`; - } - - container.innerHTML = inputHtml; - - } catch (e) { - console.warn("選項載入失敗", e); - container.innerHTML = ``; - } - } - } catch (error) { - alert("❌ 無法連線到鎖定伺服器:" + error.message); - } -} - -async function cancelEditFolder(path, elementId) { - if (ACTIVE_HEARTBEATS[path]) { - clearInterval(ACTIVE_HEARTBEATS[path]); - delete ACTIVE_HEARTBEATS[path]; - } - - try { - await fetch('/api/v1/locks/release', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ path: path, user_id: SESSION_USER_ID }) - }); - } catch (error) {} - - document.getElementById(`edit-btn-${elementId}`).style.display = 'inline-block'; - document.getElementById(`actions-${elementId}`).style.display = 'none'; - - const contentDiv = document.getElementById(`content-${elementId}`); - const leafContainers = contentDiv.querySelectorAll('.leaf-container'); - - leafContainers.forEach(container => { - const origVal = container.getAttribute('data-original'); - container.innerHTML = ` - ${origVal} - `; - }); -} - -// 🔓 取消編輯單一項目 -async function cancelEditLeaf(path, elementId) { - if (ACTIVE_HEARTBEATS[path]) { - clearInterval(ACTIVE_HEARTBEATS[path]); - delete ACTIVE_HEARTBEATS[path]; - } - - try { - await fetch('/api/v1/locks/release', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ path: path, user_id: SESSION_USER_ID }) - }); - } catch (error) {} - - document.getElementById(`edit-btn-${elementId}`).style.display = 'inline-block'; - document.getElementById(`actions-${elementId}`).style.display = 'none'; - - const container = document.getElementById(`container-${elementId}`); - const origVal = container.getAttribute('data-original'); - container.innerHTML = `${origVal}`; -} - -// 💡 輔助函數:掃描畫面上所有支援 admin-state 的區塊路徑 -function getInterfacesWithAdminState() { - const nodes = document.querySelectorAll('.leaf-container'); - const interfaces = new Set(); - - nodes.forEach(node => { - const path = node.getAttribute('data-path'); - // 如果這個節點是 admin-state - if (path && path.endsWith('::admin-state')) { - // 將路徑轉換為後端認得的介面格式 (拔除 ::admin-state、替換空白、拔除虛擬索引) - const interfacePath = path.replace('::admin-state', '').replace(/::/g, ' ').replace(/\s*\[\d+\]/g, ''); - interfaces.add(interfacePath); - } - }); - - return Array.from(interfaces); -} - -// 🚀 Phase 3: 收集修改並呼叫後端轉譯 API -async function previewFolderCLI(path, elementId) { - // 💡 記錄當前正在編輯的節點資訊 - currentEditPath = path; - currentEditElementId = elementId; - currentEditIsSuccess = false; // 預設為尚未成功 - currentEditType = 'folder'; // 💡 標記為資料夾編輯 - const contentDiv = document.getElementById(`content-${elementId}`); - if (!contentDiv) return; - - const inputs = contentDiv.querySelectorAll('.edit-input'); - const diffs = []; - - inputs.forEach(input => { - const container = input.closest('.leaf-container'); - if (!container) return; - - const originalVal = container.getAttribute('data-original') || ""; - const newVal = input.value.trim(); - const nodePath = container.getAttribute('data-path'); - - if (originalVal !== newVal && nodePath) { - diffs.push({ - path: nodePath, - old_val: originalVal, - new_val: newVal - }); - } - }); - - if (diffs.length === 0) { - alert("沒有偵測到任何修改,無需生成指令!"); - return; - } - - try { - const response = await fetch('/api/v1/cmts-generate-cli', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - diffs: diffs, - interfaces_with_admin_state: getInterfacesWithAdminState() // 💡 附上動態探測名單 - }) - }); - - const result = await response.json(); - - if (result.status === 'success') { - showCliPreviewModal(result.data); - } else { - alert("CLI 生成失敗: " + result.message); - } - } catch (error) { - console.error("Error generating CLI:", error); - alert("網路連線錯誤,無法生成 CLI"); - } -} - -// 🚀 預覽單一項目的 CLI -async function previewLeafCLI(path, elementId) { - currentEditPath = path; - currentEditElementId = elementId; - currentEditIsSuccess = false; - currentEditType = 'leaf'; // 💡 標記為單一項目編輯 - - const container = document.getElementById(`container-${elementId}`); - if (!container) return; - - const input = container.querySelector('.edit-input'); - if (!input) return; - - const originalVal = container.getAttribute('data-original') || ""; - const newVal = input.value.trim(); - - if (originalVal === newVal) return alert("沒有偵測到任何修改,無需生成指令!"); - - const diffs = [{ path: path, old_val: originalVal, new_val: newVal }]; - - try { - const response = await fetch('/api/v1/cmts-generate-cli', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - diffs: diffs, - interfaces_with_admin_state: getInterfacesWithAdminState() // 💡 附上動態探測名單 - }) - }); - - const result = await response.json(); - if (result.status === 'success') { - showCliPreviewModal(result.data); - } else { - alert("CLI 生成失敗: " + result.message); - } - } catch (error) { - alert("網路連線錯誤,無法生成 CLI"); - } -} - -// 🚀 Phase 3: 顯示側邊預覽區塊 (初始化為「預覽模式」) -function showCliPreviewModal(cliScript) { - const leftPane = document.getElementById('tree-container'); - const resizer = document.getElementById('drag-resizer'); - const rightPane = document.getElementById('side-cli-preview'); - - // --- UI 狀態重置為「預覽模式」 --- - document.getElementById('side-pane-title').innerHTML = '⚠️ 即將寫入的指令'; - document.getElementById('side-pane-title').style.color = '#f1c40f'; - - document.getElementById('btn-side-cancel').style.display = 'inline-block'; - document.getElementById('btn-side-confirm').style.display = 'inline-block'; - document.getElementById('btn-side-close').style.display = 'none'; - - document.getElementById('side-cli-textarea').style.display = 'block'; - document.getElementById('side-execution-result').style.display = 'none'; - - // 填入指令 - document.getElementById('side-cli-textarea').value = cliScript; - - // 設定初始 50/50 比例 - leftPane.style.width = 'calc(50% - 11px)'; - rightPane.style.width = 'calc(50% - 11px)'; - - resizer.style.display = 'flex'; - rightPane.style.display = 'block'; - - if (!window.isResizerInitialized) { - initResizer(); - window.isResizerInitialized = true; - } -} - -// ❌ 隱藏側邊預覽區塊 (恢復左側 100% 寬度) -async function hideSideCLI() { - document.getElementById('tree-container').style.width = '100%'; - document.getElementById('drag-resizer').style.display = 'none'; - document.getElementById('side-cli-preview').style.display = 'none'; - - // 💡 根據編輯類型,呼叫對應的套用函數 - if (currentEditIsSuccess && currentEditElementId && currentEditPath) { - - // 🔍 檢查這個節點是不是 no 指令 (透過尋找我們剛加的 display-wrapper) - const wrapper = document.getElementById(`display-wrapper-${currentEditElementId}`); - - if (wrapper) { - // 🌟 這是 no 指令的變身邏輯! - const container = document.getElementById(`container-${currentEditElementId}`); - const input = container ? container.querySelector('.edit-input') : null; - // 取得使用者編輯後的新指令 - const newCommand = input ? input.value.trim() : (container ? container.getAttribute('data-original') : ''); - - // 1. 先正常解除後端鎖定 (這會還原原本的 HTML 結構) - if (currentEditType === 'folder') { - await cancelEditFolder(currentEditPath, currentEditElementId); - } else { - await cancelEditLeaf(currentEditPath, currentEditElementId); - } - - // 2. 觸發原地變身!(這會把整個 wrapper 換成綠色打勾的樣式) - transformNoToActive(currentEditElementId, newCommand); - - } else { - // 🌟 一般正常指令的套用邏輯 (維持您原本的寫法) - if (currentEditType === 'folder') { - await applyEditFolder(currentEditPath, currentEditElementId); - } else if (currentEditType === 'leaf') { - await applyEditLeaf(currentEditPath, currentEditElementId); - } - } - } - - // 重置狀態 - currentEditPath = null; - currentEditElementId = null; - currentEditIsSuccess = false; - currentEditType = null; -} - -// 💡 新增:套用修改並解除鎖定 -async function applyEditFolder(path, elementId) { - const contentDiv = document.getElementById(`content-${elementId}`); - if (contentDiv) { - const inputs = contentDiv.querySelectorAll('.edit-input'); - inputs.forEach(input => { - const container = input.closest('.leaf-container'); - if (container) { - const newVal = input.value.trim(); - // 關鍵:將新值寫入 data-original - // 這樣稍後呼叫 cancelEditFolder 時,就會用這個新值來還原畫面 - container.setAttribute('data-original', newVal); - } - }); - } - - // 呼叫現有的 cancelEditFolder 來解除後端鎖定,並將輸入框變回純文字 - await cancelEditFolder(path, elementId); -} - -// ✅ 成功寫入後套用單一項目的新值 -async function applyEditLeaf(path, elementId) { - const container = document.getElementById(`container-${elementId}`); - if (container) { - const input = container.querySelector('.edit-input'); - if (input) { - container.setAttribute('data-original', input.value.trim()); - } - } - await cancelEditLeaf(path, elementId); -} - -// 🚀 執行側邊區塊的指令 (切換為「執行模式」) -function executeSideCLI() { - const finalScript = document.getElementById('side-cli-textarea').value; - - // --- UI 狀態切換為「執行結果模式」 --- - document.getElementById('side-pane-title').innerHTML = '⏳ 正在寫入設備...'; - document.getElementById('side-pane-title').style.color = '#3498db'; - - document.getElementById('btn-side-cancel').style.display = 'none'; - document.getElementById('btn-side-confirm').style.display = 'none'; - document.getElementById('btn-side-close').style.display = 'none'; - - document.getElementById('side-cli-textarea').style.display = 'none'; - const resultBox = document.getElementById('side-execution-result'); - resultBox.style.display = 'block'; - - // 💡 移除原本多餘的提示,直接以純白色顯示正在執行的腳本 - resultBox.innerHTML = `${finalScript}`; - - // 呼叫 API - executeGeneratedCLI(finalScript); -} - -// 🖱️ 實作左右拖曳調整寬度引擎 -function initResizer() { - const resizer = document.getElementById('drag-resizer'); - const leftPane = document.getElementById('tree-container'); - const rightPane = document.getElementById('side-cli-preview'); - const container = document.getElementById('split-container'); - - let isResizing = false; - - // 滑鼠按下分隔線:開始拖曳 - resizer.addEventListener('mousedown', (e) => { - isResizing = true; - document.body.style.cursor = 'col-resize'; - document.body.style.userSelect = 'none'; // 防止拖曳時意外反白文字 - }); - - // 滑鼠移動:動態計算寬度 - document.addEventListener('mousemove', (e) => { - if (!isResizing) return; - - const containerRect = container.getBoundingClientRect(); - let newLeftWidth = e.clientX - containerRect.left; - - // 限制左右面板的最小/最大寬度 (20% ~ 80%),防止拉過頭 - const minWidth = containerRect.width * 0.2; - const maxWidth = containerRect.width * 0.8; - if (newLeftWidth < minWidth) newLeftWidth = minWidth; - if (newLeftWidth > maxWidth) newLeftWidth = maxWidth; - - // 轉換為百分比 - const leftPercentage = (newLeftWidth / containerRect.width) * 100; - const rightPercentage = 100 - leftPercentage; - - // 即時更新左右面板寬度 - leftPane.style.width = `calc(${leftPercentage}% - 11px)`; - rightPane.style.width = `calc(${rightPercentage}% - 11px)`; - }); - - // 滑鼠放開:結束拖曳 - document.addEventListener('mouseup', () => { - if (isResizing) { - isResizing = false; - document.body.style.cursor = 'default'; - document.body.style.userSelect = 'auto'; // 恢復文字反白功能 - } - }); -} - -// 🚀 Phase 3: 將腳本發送給設備執行 (輸出至側邊欄) -async function executeGeneratedCLI(script) { - const connInfo = getGlobalConnectionInfo(); - if (!connInfo) return; - - const outputEl = document.getElementById('side-execution-result'); - - try { - const response = await fetch('/api/v1/cmts-config', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - script: script, - host: connInfo.host, - username: connInfo.user, - password: connInfo.pass - }) - }); - - const result = await response.json(); - - // 執行完畢,顯示「完成並關閉」按鈕 - document.getElementById('btn-side-close').style.display = 'inline-block'; - - if (result.status === 'success') { - currentEditIsSuccess = true; // 💡 標記執行成功 - - document.getElementById('side-pane-title').innerHTML = '✅ 執行成功'; - document.getElementById('side-pane-title').style.color = '#2ecc71'; - - // 💡 成功時,移除內部提示,直接顯示純淨的白色 Log - outputEl.innerHTML = `${result.data}`; - } else { - currentEditIsSuccess = false; // 💡 標記執行失敗 - - document.getElementById('side-pane-title').innerHTML = '❌ 執行失敗'; - document.getElementById('side-pane-title').style.color = '#ff6b6b'; - - // 💡 失敗時,移除內部提示,並透過輔助函數「僅針對錯誤行」進行紅色高亮 - const rawLog = result.detail || result.message || ''; - outputEl.innerHTML = `${highlightTerminalOutput(rawLog)}`; - } - } catch (error) { - document.getElementById('btn-side-close').style.display = 'inline-block'; - document.getElementById('side-pane-title').innerHTML = '❌ 連線錯誤'; - document.getElementById('side-pane-title').style.color = '#ff6b6b'; - - outputEl.innerHTML = `連線或伺服器錯誤: ${error}`; - } -} - -// 🔌 外掛函數:為現有的 input 加上下拉選項 (不破壞原有結構) -async function enhanceInputWithOptions(path, elementId) { - try { - const optRes = await fetch('/api/v1/cmts-leaf-options'); - const optData = await optRes.json(); - - // 🌟 建立乾淨的 cacheKey (轉換為空白格式,並全域拔除虛擬索引) - const cacheKey = path.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, ''); - const leafCache = optData[cacheKey]; - - const currentTime = Math.floor(Date.now() / 1000); - const CACHE_TTL = 86400 * 7; - - if (leafCache && leafCache.options && leafCache.options.length > 0 && (currentTime - leafCache.updated_at < CACHE_TTL)) { - // ✅ 找到選項:建立 並綁定到現有的 input 上 - const container = document.getElementById(`container-${elementId}`); - const input = container.querySelector('.edit-input'); - - if (input) { - const listId = `dl-${elementId}`; - input.setAttribute('list', listId); // 讓 input 知道要用哪個清單 - - // 建立清單元素 - let dataList = document.getElementById(listId); - if (!dataList) { - dataList = document.createElement('datalist'); - dataList.id = listId; - container.appendChild(dataList); - } - - // 填入選項 - dataList.innerHTML = leafCache.options.map(opt => `'; - statusSpan.textContent = "正在自動抓取現有 MAC Domain 清單..."; - statusSpan.style.color = "#f39c12"; - - try { - const url = `/api/v1/cmts-mac-domain-list?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}`; - const response = await fetch(url); - const result = await response.json(); - - if (result.status === 'success' && result.data.length > 0) { - 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 { - mdSelect.innerHTML = ''; - statusSpan.textContent = "無法解析清單,請確認設備狀態。"; - statusSpan.style.color = "#e74c3c"; - } - } catch (error) { - mdSelect.innerHTML = ''; - statusSpan.textContent = "連線失敗:" + error; - statusSpan.style.color = "#e74c3c"; - } -} - +// 切換查詢輸入框的啟用狀態 (全域指令不需輸入目標) function toggleQueryInputs(mode) { const type = document.getElementById('queryType' + mode).value; const targetInput = document.getElementById('queryTarget' + mode); @@ -1121,8 +134,9 @@ function toggleQueryInputs(mode) { } } +// 重置所有管理介面的資料與 UI 狀態 function resetAllManagerData() { - currentMacDomainData = null; + resetMacDomainData(); const configArea = document.getElementById('macDomainConfigArea'); if (configArea) configArea.style.display = 'none'; @@ -1152,14 +166,14 @@ function resetAllManagerData() { document.getElementById('queryTargetRpd').value = ''; } -// ========================================== -// 4. 綜合查詢 API 呼叫 -// ========================================== + +// ============================================================================ +// 🌟 3. 設備查詢與全域配置載入 (Query & Full Config) +// ============================================================================ + +// 執行綜合查詢 (CM / RPD) async function executeQuery(mode) { - if (!ws || ws.readyState !== WebSocket.OPEN) { - alert("請先點擊畫面上方的「連線至 CMTS」按鈕!"); - return; - } + if (!isConnected) return alert("請先點擊畫面上方的「連線至 CMTS」按鈕!"); const connInfo = getGlobalConnectionInfo(); if (!connInfo) return; @@ -1173,17 +187,10 @@ async function executeQuery(mode) { outputEl.innerHTML = `正在向 ${connInfo.host} 查詢中,請稍候...`; try { - const url = `/api/v1/cmts-query?query_type=${queryType}&target=${encodeURIComponent(target)}&host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}`; - const response = await fetch(url); - const result = await response.json(); - + const result = await apiExecuteQuery(queryType, target, connInfo.host, connInfo.user, connInfo.pass); if (result.status === 'success') { outputEl.style.color = "#e0e0e0"; - if (isDebug) { - outputEl.textContent = `[DEBUG] 實際發送指令: ${result.command}\n==================================================\n${result.data}`; - } else { - outputEl.textContent = result.data; - } + outputEl.textContent = isDebug ? `[DEBUG] 實際發送指令: ${result.command}\n==================================================\n${result.data}` : result.data; } else { outputEl.style.color = "#e74c3c"; outputEl.textContent = `查詢失敗:\n${result.detail || result.message}`; @@ -1194,404 +201,7 @@ async function executeQuery(mode) { } } -// ========================================== -// 5. MAC Domain 配置精靈邏輯 -// ========================================== -async function fetchMacDomainConfig() { - const target = document.getElementById('cfgMacDomain').value.trim(); - const connInfo = getGlobalConnectionInfo(); - if (!connInfo || !target) return alert("請確認設備連線資訊與 MAC Domain!"); - - document.getElementById('fetchStatus').textContent = "⏳ 正在讀取並解析設定中..."; - document.getElementById('fetchStatus').style.color = "#f39c12"; - - try { - const url = `/api/v1/cmts-mac-domain-config?target=${encodeURIComponent(target)}&host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}`; - const response = await fetch(url); - const result = await response.json(); - - if (result.status === 'success') { - currentMacDomainData = result.data; - populateFormFromData(); - document.getElementById('macDomainConfigArea').style.display = 'block'; - document.getElementById('fetchStatus').textContent = "✅ 讀取成功!"; - document.getElementById('fetchStatus').style.color = "#27ae60"; - } else { - alert("讀取失敗:" + result.detail); - document.getElementById('fetchStatus').textContent = "❌ 讀取失敗"; - } - } catch (error) { - alert("連線錯誤:" + error); - } -} - -function populateFormFromData() { - const d = currentMacDomainData; - - 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'; - document.getElementById('g_bat30').value = d.common_settings['cm-battery-mode-30-support'] || 'disabled'; - document.getElementById('g_docsis40').value = d.common_settings['docsis40'] || 'disabled'; - 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'; - - 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'] || ''; - document.getElementById('b_us_phy').value = d.basic_channel_sets['us-phy-channel-set'] || ''; - 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'] || ''; - - 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]; - - const usSelect = document.getElementById('select_us_group'); - usSelect.innerHTML = ''; - Object.keys(d.static_us_bonding_groups).forEach(grp => usSelect.innerHTML += ``); - if (Object.keys(d.static_us_bonding_groups).length > 0) usSelect.value = Object.keys(d.static_us_bonding_groups)[0]; - - toggleGroupVisibility(); - loadDsGroupData(); - loadUsGroupData(); -} - -function revertFormChanges() { - if (!currentMacDomainData) return; - if (!confirm("確定要放棄目前的修改,還原回設備原始的設定值嗎?")) return; - - populateFormFromData(); - - const previewEl = document.getElementById('cliPreviewContainer'); - if (previewEl) { - previewEl.textContent = ''; - previewEl.style.display = 'none'; - } - - const deployBtn = document.getElementById('btnDeployConfig'); - if (deployBtn) { - deployBtn.style.display = 'none'; - deployBtn.disabled = true; - } - - const fetchStatus = document.getElementById('fetchStatus'); - fetchStatus.textContent = "🔄 已還原為原始設定!"; - fetchStatus.style.color = "#2980b9"; - - setTimeout(() => { - fetchStatus.textContent = "✅ 讀取成功!"; - fetchStatus.style.color = "#27ae60"; - }, 3000); -} - -function toggleGroupVisibility() { - const dsDyn = document.getElementById('g_ds_dyn').value; - const usDyn = document.getElementById('g_us_dyn').value; - document.getElementById('section_group_ds').style.display = (dsDyn === 'disabled') ? 'block' : 'none'; - document.getElementById('section_group_us').style.display = (usDyn === 'disabled') ? 'block' : 'none'; -} - -function loadDsGroupData() { - const grp = document.getElementById('select_ds_group').value; - const newGrpInput = document.getElementById('input_new_ds_group'); - if (grp === '_new_') { - newGrpInput.style.display = 'inline-block'; - document.getElementById('ds_g_admin').value = 'down'; - document.getElementById('ds_g_down').value = ''; - document.getElementById('ds_g_ofdm').value = ''; - document.getElementById('ds_g_fdx').value = ''; - } else { - newGrpInput.style.display = 'none'; - 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'] || ''; - document.getElementById('ds_g_ofdm').value = data['ofdm-channel-set'] || ''; - document.getElementById('ds_g_fdx').value = data['fdx-ofdm-channel-set'] || ''; - } -} - -function loadUsGroupData() { - const grp = document.getElementById('select_us_group').value; - const newGrpInput = document.getElementById('input_new_us_group'); - if (grp === '_new_') { - newGrpInput.style.display = 'inline-block'; - document.getElementById('us_g_admin').value = 'down'; - document.getElementById('us_g_us').value = ''; - document.getElementById('us_g_ofdma').value = ''; - document.getElementById('us_g_fdx').value = ''; - } else { - newGrpInput.style.display = 'none'; - 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'] || ''; - document.getElementById('us_g_ofdma').value = data['ofdma-channel-set'] || ''; - document.getElementById('us_g_fdx').value = data['fdx-ofdma-channel-set'] || ''; - } -} - -function generateMacDomainCLI() { - const md = document.getElementById('cfgMacDomain').value.trim(); - let cli = `! --- Harmonic MAC Domain Configuration SOP ---\n`; - - cli += `! 1. [區塊] MAC Domain 全域與 Base 設定\n`; - cli += `cable mac-domain ${md} admin-state down\ncommit\n`; - - const baseCmds = [ - `ip-provisioning-mode ${document.getElementById('g_ip_prov').value}`, - `diplexer-band-edge control ${document.getElementById('g_diplexer').value}`, - `cm-battery-mode-31-support ${document.getElementById('g_bat31').value}`, - `cm-battery-mode-30-support ${document.getElementById('g_bat30').value}`, - `docsis40 ${document.getElementById('g_docsis40').value}`, - `ds-dynamic-bonding-group ${document.getElementById('g_ds_dyn').value}`, - `us-dynamic-bonding-group ${document.getElementById('g_us_dyn').value}`, - `ds-primary-set ${document.getElementById('b_ds_pri').value}`, - `ds-non-primary-set ${document.getElementById('b_ds_non_pri').value}`, - `us-phy-channel-set ${document.getElementById('b_us_phy').value}`, - `ds-ofdm-set ${document.getElementById('b_ds_ofdm').value}`, - `us-ofdma-set ${document.getElementById('b_us_ofdma').value}` - ]; - baseCmds.forEach(cmd => { - if (!cmd.endsWith(" ")) cli += `cable mac-domain ${md} ${cmd}\n`; - }); - cli += `cable mac-domain ${md} admin-state ${document.getElementById('b_admin').value}\ncommit\n`; - - 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; - - if (dsName) { - cli += `\n! 2. [區塊] DS Bonding Group [${dsName}] 設定\n`; - if (!isNewDs) cli += `cable mac-domain ${md} ds-bonding-group ${dsName} admin-state down\ncommit\n`; - - if (document.getElementById('ds_g_down').value) cli += `cable mac-domain ${md} ds-bonding-group ${dsName} down-channel-set ${document.getElementById('ds_g_down').value}\n`; - if (document.getElementById('ds_g_ofdm').value) cli += `cable mac-domain ${md} ds-bonding-group ${dsName} ofdm-channel-set ${document.getElementById('ds_g_ofdm').value}\n`; - if (document.getElementById('ds_g_fdx').value) cli += `cable mac-domain ${md} ds-bonding-group ${dsName} fdx-ofdm-channel-set ${document.getElementById('ds_g_fdx').value}\n`; - - cli += `cable mac-domain ${md} ds-bonding-group ${dsName} admin-state ${document.getElementById('ds_g_admin').value}\ncommit\n`; - } - } - - 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; - - if (usName) { - cli += `\n! 3. [區塊] US Bonding Group [${usName}] 設定\n`; - if (!isNewUs) cli += `cable mac-domain ${md} us-bonding-group ${usName} admin-state down\ncommit\n`; - - if (document.getElementById('us_g_us').value) cli += `cable mac-domain ${md} us-bonding-group ${usName} us-channel-set ${document.getElementById('us_g_us').value}\n`; - if (document.getElementById('us_g_ofdma').value) cli += `cable mac-domain ${md} us-bonding-group ${usName} ofdma-channel-set ${document.getElementById('us_g_ofdma').value}\n`; - if (document.getElementById('us_g_fdx').value) cli += `cable mac-domain ${md} us-bonding-group ${usName} fdx-ofdma-channel-set ${document.getElementById('us_g_fdx').value}\n`; - - cli += `cable mac-domain ${md} us-bonding-group ${usName} admin-state ${document.getElementById('us_g_admin').value}\ncommit\n`; - } - } - - const previewEl = document.getElementById('cliPreviewContainer'); - previewEl.textContent = cli; - previewEl.style.display = 'block'; - - const deployBtn = document.getElementById('btnDeployConfig'); - deployBtn.style.display = 'inline-block'; - deployBtn.disabled = false; -} - -async function executeBondingConfig() { - if (!confirm("⚠️ 警告:此操作將導致 MAC Domain 重新啟動,影響用戶服務。確定要執行嗎?")) return; - - const cliScript = document.getElementById('cliPreviewContainer').textContent; - const connInfo = getGlobalConnectionInfo(); - if (!connInfo) return; - - openModal(connInfo.host); - const outputEl = document.getElementById('modalOutput'); - outputEl.innerHTML = `🚀 正在排隊並執行配置腳本,這可能需要幾秒鐘,請勿關閉視窗...\n\n${cliScript}`; - - try { - const response = await fetch('/api/v1/cmts-config', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - script: cliScript, - host: connInfo.host, - username: connInfo.user, - password: connInfo.pass - }) - }); - const result = await response.json(); - - if (result.status === 'success') { - outputEl.style.color = "#e0e0e0"; - outputEl.textContent = `✅ 設定執行完成!\n==================================================\n${result.data}`; - } else { - outputEl.style.color = "#e74c3c"; - outputEl.textContent = `❌ 設定執行失敗:\n${result.message}`; - } - } catch (error) { - outputEl.style.color = "#e74c3c"; - outputEl.textContent = `❌ 連線或伺服器錯誤: ${error}`; - } -} - -// ========================================== -// 💡 6. 完整設備配置樹狀圖邏輯 (支援資料夾層級鎖定與虛擬群組視覺優化) -// ========================================== -// 🌟 1. 在參數中加入 isParentCommandGroup,用來實現「向下繼承」 -// 🌟 加入 isParentCommandGroup 參數,實現向下繼承 -function buildTree(node, path = '', isParentCommandGroup = false) { - let html = ''; - if (!node || typeof node !== 'object') return html; - - for (const [key, value] of Object.entries(node)) { - const currentPath = path ? `${path}::${key}` : key; - let cliCommand = currentPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, ''); - - if (typeof value === 'object' && value !== null) { - const hasNestedObject = Object.values(value).some(v => typeof v === 'object' && v !== null); - const isCommandGroup = isParentCommandGroup || !hasNestedObject; - const elementId = `folder-${currentPath.replace(/[^a-zA-Z0-9]/g, '-')}`; - const folderSuffix = isCommandGroup ? '(指令群組)' : ''; - - const svgFolderClosed = ``; - const svgFolderOpen = ``; - const svgGroupClosed = ``; - const svgGroupOpen = ``; - - const iconClosed = isCommandGroup ? svgGroupClosed : svgFolderClosed; - const iconOpen = isCommandGroup ? svgGroupOpen : svgFolderOpen; - - const expandAllIcon = ``; - const collapseAllIcon = ``; - - const expandCollapseBtns = ` - - - ${expandAllIcon} - - - ${collapseAllIcon} - - - `; - - html += ` -
- - - - ${iconClosed} - - - ${key}${folderSuffix} - ${expandCollapseBtns} - -
- - ✏️ - - -
-
-
- ${buildTree(value, currentPath, isCommandGroup)} -
-
- `; - } else { - // 🌟 處理單一設定項目 (Leaf) - const isVirtualIndex = /^\[\d+\]$/.test(key); - const isNoCommand = (key === 'no'); - const safeValue = (value === null ? '' : value).toString().replace(/"/g, """); - const elementId = `leaf-${currentPath.replace(/[^a-zA-Z0-9]/g, '-')}`; - - // 💡 終極修正:完美兼容根目錄與子目錄的 no 指令 - if (isNoCommand) { - // 1. 移除結尾的 " no" (子目錄) 或是整句剛好是 "no" (根目錄) - let baseCmd = cliCommand.replace(/(^|\s)no$/, '').trim(); - - // 2. 如果 baseCmd 有東西,代表是子目錄;如果為空,代表是根目錄 - cliCommand = baseCmd ? `${baseCmd} no ${safeValue}` : `no ${safeValue}`; - } - - const svgLeaf = ``; - const svgDisabled = ``; - - const iconHtml = (icon) => `${icon}`; - - // 💡 關鍵修正:加上 display: inline-block; 與 transform: translateY(1.5px); 強制下移 - const valueStyle = `color: #16a085; margin-left: 5px; font-family: Courier New, monospace; display: inline-block; transform: translateY(1.5px);`; - - let displayContent = ''; - - if (isVirtualIndex) { - displayContent = ` - ${iconHtml(svgLeaf)} - - ${safeValue} - - `; - } else if (isNoCommand) { - // 為了方便後續的「原地變身」,我們加上了 id="display-wrapper-..." - displayContent = ` - - ${iconHtml(svgDisabled)} - no: - - ${safeValue} - - - `; - } else { - displayContent = ` - ${iconHtml(svgLeaf)} ${key}: - - ${safeValue} - - `; - } - - html += ` -
- ${displayContent} - -
- - ✏️ - - -
-
- `; - } - } - return html; -} - +// 載入完整設備配置並生成樹狀圖 async function fetchFullConfig() { const loadingMsg = document.getElementById('loading-message'); const treeContainer = document.getElementById('tree-container'); @@ -1599,23 +209,15 @@ async function fetchFullConfig() { const connInfo = getGlobalConnectionInfo(); if (!connInfo) return; - // 💡 1. 顯示標題旁邊的載入提示 (改為 inline-block 讓它排在同一行) if (loadingMsg) loadingMsg.style.display = 'inline-block'; - - // 💡 2. 清空樹狀圖區塊,保持畫面乾淨 - if (treeContainer) { - treeContainer.innerHTML = ''; - } + if (treeContainer) treeContainer.innerHTML = ''; try { - const url = `/api/v1/cmts-full-config?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}`; - const response = await fetch(url); - const result = await response.json(); - + const result = await apiGetFullConfig(connInfo.host, connInfo.user, connInfo.pass); if (result.status === 'success') { if (treeContainer) { treeContainer.innerHTML = buildTree(result.data); - enableGlobalScanButton(); // 🌟 新增:樹狀圖成功渲染後,喚醒掃描按鈕 + enableGlobalScanButton(); } } else { if (treeContainer) treeContainer.innerHTML = `
❌ 錯誤: ${result.message}
`; @@ -1623,35 +225,234 @@ async function fetchFullConfig() { } catch (error) { if (treeContainer) treeContainer.innerHTML = `
❌ 連線或解析失敗: ${error.message}
`; } finally { - // 💡 3. 完成後隱藏載入提示 if (loadingMsg) loadingMsg.style.display = 'none'; } } -// ========================================== -// 7. 系統設定 (System Settings) 邏輯 -// ========================================== + +// ============================================================================ +// 🌟 4. 選項快取與背景掃描 (Cache & Background Scanning) +// ============================================================================ + +// 為現有的 input 加上下拉選項 (不破壞原有結構) +async function enhanceInputWithOptions(path, elementId) { + try { + const optRes = await fetch('/api/v1/cmts-leaf-options'); + const optData = await optRes.json(); + + const cacheKey = path.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, ''); + const leafCache = optData[cacheKey]; + + const currentTime = Math.floor(Date.now() / 1000); + const CACHE_TTL = 86400 * 7; + + if (leafCache && leafCache.options && leafCache.options.length > 0 && (currentTime - leafCache.updated_at < CACHE_TTL)) { + const container = document.getElementById(`container-${elementId}`); + const input = container.querySelector('.edit-input'); + + if (input) { + const listId = `dl-${elementId}`; + input.setAttribute('list', listId); + + let dataList = document.getElementById(listId); + if (!dataList) { + dataList = document.createElement('datalist'); + dataList.id = listId; + container.appendChild(dataList); + } + dataList.innerHTML = leafCache.options.map(opt => `'; + statusSpan.textContent = "正在自動抓取現有 MAC Domain 清單..."; + statusSpan.style.color = "#f39c12"; + + try { + const result = await apiGetMacDomainList(connInfo.host, connInfo.user, connInfo.pass); + if (result.status === 'success' && result.data.length > 0) { + 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 { + mdSelect.innerHTML = ''; + statusSpan.textContent = "無法解析清單,請確認設備狀態。"; + statusSpan.style.color = "#e74c3c"; + } + } catch (error) { + mdSelect.innerHTML = ''; + statusSpan.textContent = "連線失敗:" + error; + statusSpan.style.color = "#e74c3c"; + } +} + +export async function fetchMacDomainConfig() { + const target = document.getElementById('cfgMacDomain').value.trim(); + const connInfo = getGlobalConnectionInfo(); + if (!connInfo || !target) return alert("請確認設備連線資訊與 MAC Domain!"); + + document.getElementById('fetchStatus').textContent = "⏳ 正在讀取並解析設定中..."; + document.getElementById('fetchStatus').style.color = "#f39c12"; + + try { + const result = await apiGetMacDomainConfig(target, connInfo.host, connInfo.user, connInfo.pass); + if (result.status === 'success') { + currentMacDomainData = result.data; + populateFormFromData(); + document.getElementById('macDomainConfigArea').style.display = 'block'; + document.getElementById('fetchStatus').textContent = "✅ 讀取成功!"; + document.getElementById('fetchStatus').style.color = "#27ae60"; + } else { + alert("讀取失敗:" + result.detail); + document.getElementById('fetchStatus').textContent = "❌ 讀取失敗"; + } + } catch (error) { + alert("連線錯誤:" + error); + } +} + +export function populateFormFromData() { + const d = currentMacDomainData; + if (!d) return; + + 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'; + document.getElementById('g_bat30').value = d.common_settings['cm-battery-mode-30-support'] || 'disabled'; + document.getElementById('g_docsis40').value = d.common_settings['docsis40'] || 'disabled'; + 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'; + + 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'] || ''; + document.getElementById('b_us_phy').value = d.basic_channel_sets['us-phy-channel-set'] || ''; + 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'] || ''; + + 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]; + + const usSelect = document.getElementById('select_us_group'); + usSelect.innerHTML = ''; + Object.keys(d.static_us_bonding_groups).forEach(grp => usSelect.innerHTML += ``); + if (Object.keys(d.static_us_bonding_groups).length > 0) usSelect.value = Object.keys(d.static_us_bonding_groups)[0]; + + toggleGroupVisibility(); + loadDsGroupData(); + loadUsGroupData(); +} + +export function revertFormChanges() { + if (!currentMacDomainData) return; + if (!confirm("確定要放棄目前的修改,還原回設備原始的設定值嗎?")) return; + + populateFormFromData(); + + const previewEl = document.getElementById('cliPreviewContainer'); + if (previewEl) { + previewEl.textContent = ''; + previewEl.style.display = 'none'; + } + + const deployBtn = document.getElementById('btnDeployConfig'); + if (deployBtn) { + deployBtn.style.display = 'none'; + deployBtn.disabled = true; + } + + const fetchStatus = document.getElementById('fetchStatus'); + fetchStatus.textContent = "🔄 已還原為原始設定!"; + fetchStatus.style.color = "#2980b9"; + + setTimeout(() => { + fetchStatus.textContent = "✅ 讀取成功!"; + fetchStatus.style.color = "#27ae60"; + }, 3000); +} + +export function toggleGroupVisibility() { + const dsDyn = document.getElementById('g_ds_dyn').value; + const usDyn = document.getElementById('g_us_dyn').value; + document.getElementById('section_group_ds').style.display = (dsDyn === 'disabled') ? 'block' : 'none'; + document.getElementById('section_group_us').style.display = (usDyn === 'disabled') ? 'block' : 'none'; +} + +export function loadDsGroupData() { + const grp = document.getElementById('select_ds_group').value; + const newGrpInput = document.getElementById('input_new_ds_group'); + if (grp === '_new_') { + newGrpInput.style.display = 'inline-block'; + document.getElementById('ds_g_admin').value = 'down'; + document.getElementById('ds_g_down').value = ''; + document.getElementById('ds_g_ofdm').value = ''; + document.getElementById('ds_g_fdx').value = ''; + } else { + newGrpInput.style.display = 'none'; + 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'] || ''; + document.getElementById('ds_g_ofdm').value = data['ofdm-channel-set'] || ''; + document.getElementById('ds_g_fdx').value = data['fdx-ofdm-channel-set'] || ''; + } +} + +export function loadUsGroupData() { + const grp = document.getElementById('select_us_group').value; + const newGrpInput = document.getElementById('input_new_us_group'); + if (grp === '_new_') { + newGrpInput.style.display = 'inline-block'; + document.getElementById('us_g_admin').value = 'down'; + document.getElementById('us_g_us').value = ''; + document.getElementById('us_g_ofdma').value = ''; + document.getElementById('us_g_fdx').value = ''; + } else { + newGrpInput.style.display = 'none'; + 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'] || ''; + document.getElementById('us_g_ofdma').value = data['ofdma-channel-set'] || ''; + document.getElementById('us_g_fdx').value = data['fdx-ofdma-channel-set'] || ''; + } +} + +export function generateMacDomainCLI() { + const md = document.getElementById('cfgMacDomain').value.trim(); + let cli = `! --- Harmonic MAC Domain Configuration SOP ---\n`; + + cli += `! 1. [區塊] MAC Domain 全域與 Base 設定\n`; + cli += `cable mac-domain ${md} admin-state down\ncommit\n`; + + const baseCmds = [ + `ip-provisioning-mode ${document.getElementById('g_ip_prov').value}`, + `diplexer-band-edge control ${document.getElementById('g_diplexer').value}`, + `cm-battery-mode-31-support ${document.getElementById('g_bat31').value}`, + `cm-battery-mode-30-support ${document.getElementById('g_bat30').value}`, + `docsis40 ${document.getElementById('g_docsis40').value}`, + `ds-dynamic-bonding-group ${document.getElementById('g_ds_dyn').value}`, + `us-dynamic-bonding-group ${document.getElementById('g_us_dyn').value}`, + `ds-primary-set ${document.getElementById('b_ds_pri').value}`, + `ds-non-primary-set ${document.getElementById('b_ds_non_pri').value}`, + `us-phy-channel-set ${document.getElementById('b_us_phy').value}`, + `ds-ofdm-set ${document.getElementById('b_ds_ofdm').value}`, + `us-ofdma-set ${document.getElementById('b_us_ofdma').value}` + ]; + baseCmds.forEach(cmd => { + if (!cmd.endsWith(" ")) cli += `cable mac-domain ${md} ${cmd}\n`; + }); + cli += `cable mac-domain ${md} admin-state ${document.getElementById('b_admin').value}\ncommit\n`; + + 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; + + if (dsName) { + cli += `\n! 2. [區塊] DS Bonding Group [${dsName}] 設定\n`; + if (!isNewDs) cli += `cable mac-domain ${md} ds-bonding-group ${dsName} admin-state down\ncommit\n`; + + if (document.getElementById('ds_g_down').value) cli += `cable mac-domain ${md} ds-bonding-group ${dsName} down-channel-set ${document.getElementById('ds_g_down').value}\n`; + if (document.getElementById('ds_g_ofdm').value) cli += `cable mac-domain ${md} ds-bonding-group ${dsName} ofdm-channel-set ${document.getElementById('ds_g_ofdm').value}\n`; + if (document.getElementById('ds_g_fdx').value) cli += `cable mac-domain ${md} ds-bonding-group ${dsName} fdx-ofdm-channel-set ${document.getElementById('ds_g_fdx').value}\n`; + + cli += `cable mac-domain ${md} ds-bonding-group ${dsName} admin-state ${document.getElementById('ds_g_admin').value}\ncommit\n`; + } + } + + 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; + + if (usName) { + cli += `\n! 3. [區塊] US Bonding Group [${usName}] 設定\n`; + if (!isNewUs) cli += `cable mac-domain ${md} us-bonding-group ${usName} admin-state down\ncommit\n`; + + if (document.getElementById('us_g_us').value) cli += `cable mac-domain ${md} us-bonding-group ${usName} us-channel-set ${document.getElementById('us_g_us').value}\n`; + if (document.getElementById('us_g_ofdma').value) cli += `cable mac-domain ${md} us-bonding-group ${usName} ofdma-channel-set ${document.getElementById('us_g_ofdma').value}\n`; + if (document.getElementById('us_g_fdx').value) cli += `cable mac-domain ${md} us-bonding-group ${usName} fdx-ofdma-channel-set ${document.getElementById('us_g_fdx').value}\n`; + + cli += `cable mac-domain ${md} us-bonding-group ${usName} admin-state ${document.getElementById('us_g_admin').value}\ncommit\n`; + } + } + + const previewEl = document.getElementById('cliPreviewContainer'); + previewEl.textContent = cli; + previewEl.style.display = 'block'; + + const deployBtn = document.getElementById('btnDeployConfig'); + deployBtn.style.display = 'inline-block'; + deployBtn.disabled = false; +} + +export async function executeBondingConfig() { + if (!confirm("⚠️ 警告:此操作將導致 MAC Domain 重新啟動,影響用戶服務。確定要執行嗎?")) return; + + const cliScript = document.getElementById('cliPreviewContainer').textContent; + const connInfo = getGlobalConnectionInfo(); + if (!connInfo) return; + + // 開啟 Modal 顯示執行進度 + document.getElementById('outputModal').classList.add('active'); + document.getElementById('modalTargetInfo').textContent = connInfo.host ? `(${connInfo.host})` : ''; + document.body.style.overflow = 'hidden'; + + const outputEl = document.getElementById('modalOutput'); + outputEl.innerHTML = `🚀 正在排隊並執行配置腳本,這可能需要幾秒鐘,請勿關閉視窗...\n\n${cliScript}`; + + try { + const result = await apiExecuteConfig(cliScript, connInfo.host, connInfo.user, connInfo.pass); + if (result.status === 'success') { + outputEl.style.color = "#e0e0e0"; + outputEl.textContent = `✅ 設定執行完成!\n==================================================\n${result.data}`; + } else { + outputEl.style.color = "#e74c3c"; + outputEl.textContent = `❌ 設定執行失敗:\n${result.message}`; + } + } catch (error) { + outputEl.style.color = "#e74c3c"; + outputEl.textContent = `❌ 連線或伺服器錯誤: ${error}`; + } +} diff --git a/static/tree-ui.js b/static/tree-ui.js new file mode 100644 index 0000000..989ed3c --- /dev/null +++ b/static/tree-ui.js @@ -0,0 +1,279 @@ +// --- static/tree-ui.js --- + +// ========================================== +// 🌟 輔助函數:全部展開與全部收合 +// ========================================== +export function expandAll(elementId) { + const details = document.getElementById(`details-${elementId}`); + if (details) { + details.open = true; // 展開自己 + const subDetails = details.querySelectorAll('details'); + subDetails.forEach(d => d.open = true); // 展開所有子項目 + } +} + +export function collapseAll(elementId) { + const details = document.getElementById(`details-${elementId}`); + if (details) { + const subDetails = details.querySelectorAll('details'); + subDetails.forEach(d => d.open = false); // 收合所有子項目 + details.open = false; // 收合自己 + } +} + +// ========================================== +// 🌟 核心函數:生成完整設備配置樹狀圖 +// ========================================== +export function buildTree(node, path = '', isParentCommandGroup = false) { + let html = ''; + if (!node || typeof node !== 'object') return html; + + for (const [key, value] of Object.entries(node)) { + const currentPath = path ? `${path}::${key}` : key; + let cliCommand = currentPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, ''); + + if (typeof value === 'object' && value !== null) { + const hasNestedObject = Object.values(value).some(v => typeof v === 'object' && v !== null); + const isCommandGroup = isParentCommandGroup || !hasNestedObject; + const elementId = `folder-${currentPath.replace(/[^a-zA-Z0-9]/g, '-')}`; + const folderSuffix = isCommandGroup ? '(指令群組)' : ''; + + const svgFolderClosed = ``; + const svgFolderOpen = ``; + const svgGroupClosed = ``; + const svgGroupOpen = ``; + + const iconClosed = isCommandGroup ? svgGroupClosed : svgFolderClosed; + const iconOpen = isCommandGroup ? svgGroupOpen : svgFolderOpen; + + const expandAllIcon = ``; + const collapseAllIcon = ``; + + const expandCollapseBtns = ` + + + ${expandAllIcon} + + + ${collapseAllIcon} + + + `; + + html += ` +
+ + + + ${iconClosed} + + + ${key}${folderSuffix} + ${expandCollapseBtns} + +
+ + ✏️ + + +
+
+
+ ${buildTree(value, currentPath, isCommandGroup)} +
+
+ `; + } else { + const isVirtualIndex = /^\[\d+\]$/.test(key); + const isNoCommand = (key === 'no'); + const safeValue = (value === null ? '' : value).toString().replace(/"/g, """); + const elementId = `leaf-${currentPath.replace(/[^a-zA-Z0-9]/g, '-')}`; + + if (isNoCommand) { + let baseCmd = cliCommand.replace(/(^|\s)no$/, '').trim(); + cliCommand = baseCmd ? `${baseCmd} no ${safeValue}` : `no ${safeValue}`; + } + + const svgLeaf = ``; + const svgDisabled = ``; + + const iconHtml = (icon) => `${icon}`; + const valueStyle = `color: #16a085; margin-left: 5px; font-family: Courier New, monospace; display: inline-block; transform: translateY(1.5px);`; + + let displayContent = ''; + + if (isVirtualIndex) { + displayContent = ` + ${iconHtml(svgLeaf)} + + ${safeValue} + + `; + } else if (isNoCommand) { + displayContent = ` + + ${iconHtml(svgDisabled)} + no: + + ${safeValue} + + + `; + } else { + displayContent = ` + ${iconHtml(svgLeaf)} ${key}: + + ${safeValue} + + `; + } + + html += ` +
+ ${displayContent} + +
+ + ✏️ + + +
+
+ `; + } + } + return html; +} + +// ========================================== +// 🌟 核心函數:生成系統設定的過濾樹狀圖 +// ========================================== +export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isParentCommandGroup = false) { + if (typeof data !== 'object' || data === null) return ''; + + let html = `
`; + + for (const key in data) { + const value = data[key]; + const currentPath = parentPath ? `${parentPath}::${key}` : key; + const isChecked = hiddenKeys.includes(currentPath) ? "checked" : ""; + + let cliCommand = currentPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, ''); + + const svgFolderClosed = ``; + const svgFolderOpen = ``; + const svgGroupClosed = ``; + const svgGroupOpen = ``; + const svgLeaf = ``; + + const isFolder = typeof value === 'object' && value !== null && Object.keys(value).length > 0; + const onChangeEvent = isFolder ? 'toggleChildCheckboxes(this)' : 'updateParentCheckboxState(this)'; + const checkboxHtml = ``; + + if (isFolder) { + const hasNestedObject = Object.values(value).some(v => typeof v === 'object' && v !== null); + const isCommandGroup = isParentCommandGroup || !hasNestedObject; + const elementId = `filter-folder-${currentPath.replace(/[^a-zA-Z0-9]/g, '-')}`; + + const iconClosed = isCommandGroup ? svgGroupClosed : svgFolderClosed; + const iconOpen = isCommandGroup ? svgGroupOpen : svgFolderOpen; + const folderSuffix = isCommandGroup ? `(指令群組)` : ''; + + const expandAllIcon = ``; + const collapseAllIcon = ``; + + const expandCollapseBtns = ` + + + ${expandAllIcon} + + + ${collapseAllIcon} + + + `; + + html += ` +
+ + + ${checkboxHtml} + + ${iconClosed} + + + ${key}${folderSuffix} + ${expandCollapseBtns} + +
+ ${buildRealFilterTree(value, currentPath, hiddenKeys, isCommandGroup)} +
+
+ `; + } else { + const isVirtualIndex = /^\[\d+\]$/.test(key); + let displayName = isVirtualIndex ? `項目 ${key}` : `${key}`; + const safeValue = (value === null ? '' : value).toString().replace(/"/g, """); + + const isNoCommand = (key === 'no'); + + let valueHtml = `${safeValue}`; + let colonHtml = `:`; + let currentIcon = ``; + + if (isNoCommand) { + let baseCmd = cliCommand.replace(/(^|\s)no$/, '').trim(); + cliCommand = baseCmd ? `${baseCmd} no ${safeValue}` : `no ${safeValue}`; + + displayName = `no`; + colonHtml = `:`; + valueHtml = `${safeValue}`; + currentIcon = ``; + } + + html += ` +
+ ${checkboxHtml} + + ${currentIcon} + + ${displayName}${colonHtml} + ${valueHtml} +
+ `; + } + } + html += '
'; + return html; +} diff --git a/system_settings.json b/system_settings.json index 2988058..43ac382 100644 --- a/system_settings.json +++ b/system_settings.json @@ -1,6 +1,5 @@ { "hidden_keys": [ - "ssh", - "clock" + "ssh" ] } \ No newline at end of file