/* --- static/app.js --- */ // ========================================== // 1. 全域變數與初始化 // ========================================== let term, fitAddon, ws; let currentMacDomainData = null; let isConnected = false; // 💡 新增:用來追蹤目前的連線狀態 window.onload = initTerminal; window.onresize = () => { if (fitAddon) fitAddon.fit(); }; // ========================================== // 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); // 💡 已經移除自動觸發 switchConfigTask(),避免切換頁籤時覆蓋使用者資料 } // 💡 查詢頁籤專用的切換邏輯 function switchQueryTask() { // 只隱藏 query-tab 裡面的表單 document.querySelectorAll('#query-tab .task-form').forEach(form => form.classList.remove('active')); const selectedTaskId = document.getElementById('queryTask').value; const targetForm = document.getElementById(selectedTaskId); if (targetForm) targetForm.classList.add('active'); } // 💡 配置頁籤專用的切換邏輯 (現在只負責切換表單顯示,不自動抓資料) function switchConfigTask() { // 只隱藏 config-tab 裡面的表單 document.querySelectorAll('#config-tab .task-form').forEach(form => form.classList.remove('active')); const selectedTaskId = document.getElementById('configTask').value; const targetForm = document.getElementById(selectedTaskId); if (targetForm) targetForm.classList.add('active'); } // 💡 新增的函數:由「🚀 載入任務」按鈕手動觸發 function startConfigTask() { // 💡 新增防呆:雙重檢查是否已連線 if (!isConnected) { alert("請先成功連線至 CMTS 設備後,再執行載入任務!"); return; } // 1. 先確保對應的表單有顯示出來 switchConfigTask(); // 2. 根據選擇的任務,決定要向設備抓取什麼資料 const selectedTaskId = document.getElementById('configTask').value; if (selectedTaskId === 'form-bonding-config') { // 如果使用者正在編輯到一半,給予警告提示 if (currentMacDomainData && !confirm("重新載入將會清除您目前未套用的設定,確定要繼續嗎?")) { return; // 使用者取消,中斷執行 } // 確定載入,重置畫面並向設備抓取 MAC Domain 清單 resetAllManagerData(); loadMacDomainList(); } } // 💡 新增函數:向後端請求 MAC Domain 清單並動態生成下拉選單 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 (留白則查詢全體)"; } } // 💡 新增函數:重置所有 Manager 頁籤的暫存資料與畫面 function resetAllManagerData() { // 1. 清空記憶體中的設定資料 currentMacDomainData = null; // 2. 隱藏 MAC Domain 配置表單與 CLI 預覽區塊 const configArea = document.getElementById('macDomainConfigArea'); if (configArea) configArea.style.display = 'none'; const previewEl = document.getElementById('cliPreviewContainer'); if (previewEl) { previewEl.textContent = ''; previewEl.style.display = 'none'; } // 3. 隱藏並禁用「正式套用設定」按鈕 const deployBtn = document.getElementById('btnDeployConfig'); if (deployBtn) { deployBtn.style.display = 'none'; deployBtn.disabled = true; } // 4. 重置 MAC Domain 下拉選單與狀態文字 const mdSelect = document.getElementById('cfgMacDomain'); if (mdSelect) mdSelect.innerHTML = ''; const fetchStatus = document.getElementById('fetchStatus'); if (fetchStatus) { fetchStatus.textContent = "請先讀取設備狀態..."; fetchStatus.style.color = "#7f8c8d"; } // 5. (選擇性) 清空查詢表單的輸入框 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; // 💡 關鍵修改 1:在發起新連線前,強制清空舊設備的所有殘留資料 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'); // 💡 關鍵修改 2:斷線時也自動重置畫面 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; // 💡 正名工程:對齊後端 common_settings document.getElementById('g_ip_prov').value = d.common_settings['ip-provisioning-mode'] || 'dual-stack'; document.getElementById('g_diplexer').value = d.common_settings['diplexer-band-edge'] || 'enabled'; document.getElementById('g_bat31').value = d.common_settings['cm-battery-mode-31-support'] || 'disabled'; 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'; // 💡 正名工程:對齊後端 basic_channel_sets document.getElementById('b_admin').value = d.basic_channel_sets['admin-state'] || 'down'; document.getElementById('b_ds_pri').value = d.basic_channel_sets['ds-primary-set'] || ''; document.getElementById('b_ds_non_pri').value = d.basic_channel_sets['ds-non-primary-set'] || ''; 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'] || ''; // 💡 正名工程:對齊後端 static_ds_bonding_groups const dsSelect = document.getElementById('select_ds_group'); dsSelect.innerHTML = ''; Object.keys(d.static_ds_bonding_groups).forEach(grp => dsSelect.innerHTML += ``); if (Object.keys(d.static_ds_bonding_groups).length > 0) dsSelect.value = Object.keys(d.static_ds_bonding_groups)[0]; // 💡 正名工程:對齊後端 static_us_bonding_groups const usSelect = document.getElementById('select_us_group'); usSelect.innerHTML = ''; Object.keys(d.static_us_bonding_groups).forEach(grp => usSelect.innerHTML += ``); 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; } // 1. 重新利用記憶體中的原始 JSON 資料填入表單 populateFormFromData(); // 2. 隱藏已經產生的 CLI 預覽與套用按鈕 (因為狀態已經改變) 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; } // 3. 給予使用者視覺回饋 const fetchStatus = document.getElementById('fetchStatus'); fetchStatus.textContent = "🔄 已還原為原始設定!"; fetchStatus.style.color = "#2980b9"; // 3秒後把狀態文字改回綠色的讀取成功 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'; // 💡 正名工程:對齊後端 static_ds_bonding_groups const data = currentMacDomainData.static_ds_bonding_groups[grp]; document.getElementById('ds_g_admin').value = data['admin-state'] || 'down'; document.getElementById('ds_g_down').value = data['down-channel-set'] || ''; 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'; // 💡 正名工程:對齊後端 static_us_bonding_groups const data = currentMacDomainData.static_us_bonding_groups[grp]; document.getElementById('us_g_admin').value = data['admin-state'] || 'down'; document.getElementById('us_g_us').value = data['us-channel-set'] || ''; 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`; // --- 區塊 1: MAC Domain Base --- 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`; // --- 區塊 2: DS Bonding Group --- if (document.getElementById('g_ds_dyn').value === 'disabled') { const isNewDs = document.getElementById('select_ds_group').value === '_new_'; let dsName = isNewDs ? document.getElementById('input_new_ds_group').value.trim() : document.getElementById('select_ds_group').value; 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`; } } // --- 區塊 3: US Bonding Group --- if (document.getElementById('g_us_dyn').value === 'disabled') { const isNewUs = document.getElementById('select_us_group').value === '_new_'; let usName = isNewUs ? document.getElementById('input_new_us_group').value.trim() : document.getElementById('select_us_group').value; 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. Modal 彈出視窗控制 // ========================================== function openModal(targetInfo) { document.getElementById('outputModal').classList.add('active'); document.getElementById('modalTargetInfo').textContent = targetInfo ? `(${targetInfo})` : ''; document.body.style.overflow = 'hidden'; } function closeModal(event) { if (event && event.target !== event.currentTarget && event.target.className !== 'modal-close') return; document.getElementById('outputModal').classList.remove('active'); document.body.style.overflow = ''; }