/* --- static/app.js --- */ // ========================================== // 1. 全域變數與初始化 // ========================================== let term, fitAddon, ws; let currentMacDomainData = null; let isConnected = false; window.onload = initTerminal; window.onresize = () => { if (fitAddon) fitAddon.fit(); }; // ========================================== // 💡 全域狀態與鎖定管理 (Lock Management) // ========================================== const SESSION_USER_ID = crypto.randomUUID ? crypto.randomUUID() : 'user-' + Math.random().toString(36).substr(2, 9); const ACTIVE_HEARTBEATS = {}; // 記錄正在打心跳的計時器: { "path": intervalId } async function sendHeartbeat(path) { try { await fetch('/api/v1/locks/heartbeat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: path, user_id: SESSION_USER_ID }) }); } catch (error) {} } // ========================================== // 💡 資料夾層級的編輯模式邏輯 (Node-Level Lock) // ========================================== 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); // UI 切換 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'); leafContainers.forEach(container => { const origVal = container.getAttribute('data-original'); const nodePath = container.getAttribute('data-path'); 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} `; }); } function previewFolderCLI(path, elementId) { const contentDiv = document.getElementById(`content-${elementId}`); const inputs = contentDiv.querySelectorAll('.edit-input'); let changes = []; // 掃描所有輸入框,找出被修改過的值 inputs.forEach(input => { const nodePath = input.getAttribute('data-path'); const newVal = input.value; const origVal = input.parentElement.getAttribute('data-original'); if (newVal !== origVal) { changes.push(`- ${nodePath}:\n [舊] ${origVal} ➡️ [新] ${newVal}`); } }); if (changes.length === 0) { alert("您沒有修改任何數值!"); return; } alert(`【Phase 3 預覽】\n鎖定區塊: ${path}\n\n偵測到的修改:\n${changes.join('\n\n')}\n\n即將實作轉譯為 CMTS CLI 的功能!`); } // ========================================== // 💡 編輯模式 UI 切換邏輯 // ========================================== function renderEditModeUI(path, currentValue, elementId) { const container = document.getElementById(elementId); if (!container) return; // 將原本的文字替換為 Input 與操作按鈕 // 注意:這裡將單引號跳脫,避免 HTML 屬性解析錯誤 const safeValue = currentValue.replace(/'/g, "'"); container.innerHTML = ` `; } function restoreViewModeUI(path, originalValue, elementId) { const container = document.getElementById(elementId); if (!container) return; // 恢復成原本的文字與編輯按鈕 const safeValue = originalValue.replace(/'/g, "'"); container.innerHTML = ` ${originalValue} ✏️ `; } // Phase 3 的預留函數:預覽 CLI 指令 function previewCLI(path, elementId) { const newValue = document.getElementById(`input-${elementId}`).value; alert(`【Phase 3 預覽】\n路徑: ${path}\n新值: ${newValue}\n\n即將實作轉譯為 CMTS CLI 的功能!`); } // ========================================== // 2. UI 互動與頁籤切換 // ========================================== function switchTab(tabId, element) { document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active')); document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); const targetTab = document.getElementById(tabId); if (targetTab) targetTab.classList.add('active'); if (element) element.classList.add('active'); if(tabId === 'cli-tab' && fitAddon) setTimeout(() => fitAddon.fit(), 50); } function switchQueryTask() { 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() { 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'); targetForm.style.display = 'block'; } } // 💡 合併修復:統一處理「載入任務」的邏輯與防呆 function startConfigTask() { // 防呆 1:檢查是否已連線 if (!isConnected) { alert("請先點擊畫面上方的「連線至 CMTS」成功後,再執行載入任務!"); return; } // 先確保對應的表單有顯示出來 switchConfigTask(); const selectedTaskId = document.getElementById('configTask').value; if (selectedTaskId === 'form-bonding-config') { // 任務 A:MAC Domain 配置精靈 if (currentMacDomainData && !confirm("重新載入將會清除您目前未套用的設定,確定要繼續嗎?")) { return; } resetAllManagerData(); loadMacDomainList(); } else if (selectedTaskId === 'form-full-config') { // 💡 修復:任務 B:完整設備配置樹狀圖 // 當使用者點擊「載入任務」時,直接幫他觸發抓取樹狀圖的 API fetchFullConfig(); } } async function loadMacDomainList() { const connInfo = getGlobalConnectionInfo(); if (!connInfo) return; const mdSelect = document.getElementById('cfgMacDomain'); const statusSpan = document.getElementById('fetchStatus'); mdSelect.innerHTML = ''; 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); const globalTypes = ['partial_mode', 'hop', 'flap_list', 'flap_sum']; if (globalTypes.includes(type)) { targetInput.disabled = true; targetInput.style.backgroundColor = '#e8ecef'; targetInput.placeholder = "此查詢為全域指令,不需輸入目標"; targetInput.value = ""; } else { targetInput.disabled = false; targetInput.style.backgroundColor = '#f8f9fa'; targetInput.placeholder = mode === 'Cm' ? "例: 6467.7240.4076 (留白則查詢全體)" : "例: 13:0 (留白則查詢全體)"; } } function resetAllManagerData() { currentMacDomainData = null; const configArea = document.getElementById('macDomainConfigArea'); if (configArea) configArea.style.display = 'none'; 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 mdSelect = document.getElementById('cfgMacDomain'); if (mdSelect) mdSelect.innerHTML = ''; const fetchStatus = document.getElementById('fetchStatus'); if (fetchStatus) { fetchStatus.textContent = "請先讀取設備狀態..."; fetchStatus.style.color = "#7f8c8d"; } document.getElementById('queryTargetCm').value = ''; document.getElementById('queryTargetRpd').value = ''; } // ========================================== // 3. 終端機 (Terminal) 與 WebSocket 邏輯 // ========================================== function colorizeTerminalStream(text) { if (text.includes('\x1b[')) return text; let res = text; res = res.replace(/\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g, '\x1b[1;34m$1\x1b[0m'); res = res.replace(/\b([0-9a-fA-F]{4}\.[0-9a-fA-F]{4}\.[0-9a-fA-F]{4}|[0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})\b/g, '\x1b[1;35m$1\x1b[0m'); res = res.replace(/\b(online|w-online|p-online|init\([^)]+\)|up|active|success|yes)\b/gi, '\x1b[1;32m$1\x1b[0m'); res = res.replace(/\b(offline|down|admin-down|error|fail|failed|shutdown|reject|invalid|no)\b/gi, '\x1b[1;31m$1\x1b[0m'); res = res.replace(/\b(Cable|GigabitEthernet|TenGigabitEthernet|Upstream|Downstream|Mac-domain|bonding-group|rpd)\b/gi, '\x1b[33m$1\x1b[0m'); return res; } function initTerminal() { term = new Terminal({ cursorBlink: true, theme: { background: '#1e1e1e', foreground: '#e0e0e0' }, fontFamily: 'Courier New, monospace', fontSize: 15, scrollback: 100000 }); fitAddon = new FitAddon.FitAddon(); term.loadAddon(fitAddon); term.open(document.getElementById('terminal-container')); fitAddon.fit(); term.writeln('Welcome to Harmonic cOS Web Terminal.'); term.writeln('Please confirm the target device above and click "連線至 CMTS"... \r\n'); term.onData((data) => { if (ws && ws.readyState === WebSocket.OPEN) ws.send(data); }); } function getGlobalConnectionInfo() { const host = document.getElementById('cmtsHost').value.trim(); const user = document.getElementById('cmtsUser').value.trim(); const pass = document.getElementById('cmtsPass').value.trim(); if (!host || !user || !pass) { alert("請在畫面上方完整輸入目標設備的 IP、帳號與密碼!"); return null; } return { host, user, pass }; } function connectWebSocket() { if (ws && ws.readyState === WebSocket.OPEN) return; const connInfo = getGlobalConnectionInfo(); if (!connInfo) return; resetAllManagerData(); term.writeln(`\x1b[33mConnecting to ${connInfo.host} via WebSocket...\x1b[0m`); const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws/terminal?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}`; ws = new WebSocket(wsUrl); ws.onopen = () => { isConnected = true; const btnLoadTask = document.getElementById('btn-load-task'); if (btnLoadTask) { btnLoadTask.disabled = false; btnLoadTask.style.backgroundColor = '#c0392b'; btnLoadTask.style.cursor = 'pointer'; } document.getElementById('wsStatus').textContent = `狀態:已連線`; document.getElementById('wsStatus').style.color = '#27ae60'; document.getElementById('btnConnect').style.display = 'none'; document.getElementById('btnDisconnect').style.display = 'inline-block'; document.getElementById('cmtsHost').disabled = true; document.getElementById('cmtsUser').disabled = true; document.getElementById('cmtsPass').disabled = true; term.focus(); }; ws.onmessage = (event) => { term.write(colorizeTerminalStream(event.data)); }; ws.onclose = () => { isConnected = false; const btnLoadTask = document.getElementById('btn-load-task'); if (btnLoadTask) { btnLoadTask.disabled = true; btnLoadTask.style.backgroundColor = '#95a5a6'; btnLoadTask.style.cursor = 'not-allowed'; } document.getElementById('wsStatus').textContent = '狀態:已斷線'; document.getElementById('wsStatus').style.color = '#c0392b'; document.getElementById('btnConnect').style.display = 'inline-block'; document.getElementById('btnDisconnect').style.display = 'none'; document.getElementById('cmtsHost').disabled = false; document.getElementById('cmtsUser').disabled = false; document.getElementById('cmtsPass').disabled = false; term.writeln('\r\n\x1b[31m--- Connection Closed ---\x1b[0m\r\n'); resetAllManagerData(); }; } function disconnectWebSocket() { if (ws) ws.close(); } function injectCommand() { if (!ws || ws.readyState !== WebSocket.OPEN) return alert("請先點擊「連線至 CMTS」!"); const cmd = document.getElementById('fixedCmd').value; ws.send(cmd + '\r'); term.focus(); } function getTerminalText() { if (!term) return ''; try { if (term.hasSelection()) return term.getSelection(); const buffer = term.buffer.active; let text = ''; for (let i = 0; i < buffer.length; i++) { const line = buffer.getLine(i); if (line) text += line.translateToString(true) + '\n'; } return text; } catch (err) { console.error("讀取終端機文字失敗:", err); return ''; } } function downloadTerminal() { const text = getTerminalText(); if (!text.trim()) return alert("沒有內容可下載!"); try { const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19); a.download = `cmts_terminal_log_${timestamp}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (err) { console.error("下載失敗:", err); } } // ========================================== // 4. 綜合查詢 API 呼叫 // ========================================== async function executeQuery(mode) { if (!ws || ws.readyState !== WebSocket.OPEN) { alert("請先點擊畫面上方的「連線至 CMTS」按鈕!"); return; } const connInfo = getGlobalConnectionInfo(); if (!connInfo) return; const queryType = document.getElementById('queryType' + mode).value; const target = document.getElementById('queryTarget' + mode).value.trim(); const isDebug = document.getElementById('debugQuery' + mode).checked; openModal(connInfo.host); const outputEl = document.getElementById('modalOutput'); 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(); if (result.status === 'success') { outputEl.style.color = "#e0e0e0"; if (isDebug) { outputEl.textContent = `[DEBUG] 實際發送指令: ${result.command}\n==================================================\n${result.data}`; } else { outputEl.textContent = result.data; } } else { outputEl.style.color = "#e74c3c"; outputEl.textContent = `查詢失敗:\n${result.detail || result.message}`; } } catch (error) { outputEl.style.color = "#e74c3c"; outputEl.textContent = `連線失敗: ${error}`; } } // ========================================== // 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. 完整設備配置樹狀圖邏輯 (支援資料夾層級鎖定) // ========================================== function buildTree(data, parentPath = "") { if (typeof data !== 'object' || data === null) { return `${data}`; } let html = '