+
+
+
+
+
- // 🌟 關鍵修復:只要收到「開始」或「進度」訊號,一律強制鎖定按鈕!(確保中途加入的使用者也會被鎖定)
- if (data.event === 'scan_start' || data.event === 'progress') {
- setAdminButtonsState(true, "⏳ 系統更新中...");
- }
-
- if (data.event === 'scan_start') {
- if (statusEl) {
- statusEl.innerHTML = `⏳ 系統正在背景更新快取,請稍候...`;
- statusEl.style.color = "#f39c12";
- }
- }
- else if (data.event === 'progress') {
- if (statusEl) {
- statusEl.innerHTML = `⏳ 背景掃描進度:
${data.current} / ${data.total} (正在處理: ${data.current_path})`;
- statusEl.style.color = "#f39c12";
- }
- }
- else if (data.event === 'done') {
- if (statusEl) {
- statusEl.innerHTML = `✅ 掃描完美達成!快取已全面更新。`;
- statusEl.style.color = "#27ae60";
- setTimeout(() => statusEl.textContent = "", 5000);
- }
- // 掃描完成才解鎖
- setAdminButtonsState(false);
- localStorage.removeItem('scanLockUntil');
- }
- else if (data.event === 'error') {
- if (statusEl) {
- statusEl.innerHTML = `❌ 錯誤: ${data.message}`;
- statusEl.style.color = "#e74c3c";
- }
- // 發生錯誤也要解鎖
- setAdminButtonsState(false);
- localStorage.removeItem('scanLockUntil');
- }
- };
-}
-
-// 🌟 關鍵修復:當使用者按 F5 重整或關閉網頁時,主動切斷 SSE 連線
-window.addEventListener('beforeunload', () => {
- if (globalSSE) {
- globalSSE.close();
- globalSSE = null;
- }
-});
-
-// 🌟 特務的記憶體:記錄「上一次輪詢時,處於鎖定狀態的路徑」(加入 Host 隔離)
-let activeLockState = new Set();
-let lockPollingTimer = null;
-let currentPollingHost = null; // 記錄當前輪詢的設備 IP
-window.globalActiveLocks = {}; // 🌟 升級為二維結構:{ "10.14.110.4": { "alias": {...} } }
-window.recentlyReleasedLocks = {}; // 🌟 新增:防閃爍冷卻表 (Optimistic UI Cooldown)
-
-function startLockStatusPolling() {
- if (lockPollingTimer) clearInterval(lockPollingTimer);
-
- lockPollingTimer = setInterval(async () => {
- const connInfo = getGlobalConnectionInfo();
- if (!connInfo || !connInfo.host) return;
- const host = connInfo.host;
-
- // 🛡️ 跨設備防呆:如果使用者切換了 IP,必須清空舊設備的鎖定記憶,防止幽靈解鎖
- if (currentPollingHost !== host) {
- activeLockState.clear();
- currentPollingHost = host;
- }
-
- try {
- const result = await apiGetLockStatus(host);
- if (result.status === 'success') {
- const currentLocks = result.data;
- window.globalActiveLocks[host] = currentLocks;
- const newLockState = new Set();
- const now = Date.now();
-
- // 🎯 任務 1:處理「新增鎖定」或「持續鎖定」的節點
- for (const [path, lockInfo] of Object.entries(currentLocks)) {
- const lockKey = `${host}@@${path}`;
-
- // 🛡️ 防閃爍機制:如果這個鎖是我們剛剛才主動釋放的,忽略後端傳來的舊狀態 (冷卻 8 秒)
- if (window.recentlyReleasedLocks[lockKey]) {
- if (now - window.recentlyReleasedLocks[lockKey] < 8000) {
- continue; // 跳過,不當作鎖定
- } else {
- delete window.recentlyReleasedLocks[lockKey]; // 超過冷卻時間,清除記憶
- }
- }
-
- newLockState.add(lockKey); // 記憶體加入 Host 標籤
-
- const safePath = path.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
- // 🌟 擴大選擇器:同時選取完全匹配的節點,以及所有子節點
- const targetBtns = document.querySelectorAll(`.edit-btn[data-path="${safePath}"], .edit-btn[data-path^="${safePath}::"]`);
-
- targetBtns.forEach(btn => {
- // 略過目前正在編輯的那個實體按鈕
- if (btn.id === `edit-btn-${currentEditElementId}`) return;
-
- if (btn.innerText !== "🔒") {
- btn.style.pointerEvents = 'none';
- btn.style.opacity = '0.2';
-
- const btnPath = btn.getAttribute('data-path');
- if (btnPath === path) {
- // 判斷是別人鎖的,還是自己在另一個視圖鎖的
- if (lockInfo.user_id !== SESSION_USER_ID) {
- btn.title = `🔒 此區塊正由 [${lockInfo.username}] 編輯中`;
- } else {
- btn.title = `🔒 您正在另一個視圖編輯此項目`;
- }
- } else {
- // 子節點被父節點鎖定
- btn.title = `🔒 父層級已被鎖定,無法編輯此項目`;
- }
- btn.innerText = "🔒";
- }
- });
- }
-
- // 🔓 任務 2:處理「解除鎖定」的節點
- activeLockState.forEach(oldKey => {
- const [oldHost, oldPath] = oldKey.split('@@');
-
- // 🛡️ 嚴格限制:只解除「當前設備」且「已不在新鎖定清單中」的節點
- if (oldHost === host && !newLockState.has(oldKey)) {
- const safePath = oldPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
- // 🌟 擴大選擇器:同時選取完全匹配的節點,以及所有子節點
- const targetBtns = document.querySelectorAll(`.edit-btn[data-path="${safePath}"], .edit-btn[data-path^="${safePath}::"]`);
-
- targetBtns.forEach(btn => {
- if (btn.innerText === "🔒") {
- const btnPath = btn.getAttribute('data-path');
- let stillLockedByOther = false;
-
- // 檢查自己是否還在鎖定清單中
- if (currentLocks[btnPath]) {
- stillLockedByOther = true;
- } else {
- // 檢查是否有其他父節點鎖定它
- for (const lockedPath of Object.keys(currentLocks)) {
- if (btnPath.startsWith(lockedPath + "::")) {
- stillLockedByOther = true;
- break;
- }
- }
- }
-
- if (!stillLockedByOther) {
- btn.style.pointerEvents = 'auto';
- btn.style.opacity = '0.3';
- btn.title = "鎖定並編輯此項目";
- btn.innerText = "✏️";
- }
- }
- });
- }
- });
-
- // 💾 任務 3:更新特務的記憶
- activeLockState = newLockState;
- }
- } catch (error) {
- console.warn("獲取鎖定狀態失敗:", error);
- }
- }, 15000);
-}
-
-// 頁面載入與縮放初始化
-window.onload = () => {
- initTerminal();
- resetIdleTimer(); // 頁面載入時啟動閒置計時器
- initGlobalSSE(); // 啟動全域廣播監聽
- startLockStatusPolling(); // 啟動鎖定狀態輪詢
-
- // 🌟 新增:網頁載入時,自動觸發一次「選擇配置任務」的切換,確保畫面與選單同步
- const configTaskSelect = document.getElementById('configTask');
- if (configTaskSelect) {
- configTaskSelect.dispatchEvent(new Event('change'));
- }
-};
-window.onresize = () => { if (typeof fitAddon !== 'undefined' && fitAddon) fitAddon.fit(); };
-
-// 閒置計時器重置邏輯 (超時將自動釋放所有編輯鎖定)
-function resetIdleTimer() {
- clearTimeout(idleTimer);
- idleTimer = setTimeout(releaseAllLocks, IDLE_TIMEOUT);
-}
-
-// 🌟 效能急救:節流閥 (Throttle) 機制
-let throttleTimer = false;
-function throttledReset() {
- if (!throttleTimer) {
- resetIdleTimer();
- throttleTimer = true;
- setTimeout(() => { throttleTimer = false; }, 2000); // 每 2 秒最多只觸發一次重設
- }
-}
-window.addEventListener('mousemove', throttledReset);
-window.addEventListener('keydown', throttledReset);
-window.addEventListener('click', throttledReset);
-
-// ============================================================================
-// 🌟 2. 頁籤切換與 UI 狀態控制 (Tabs & UI State)
-// ============================================================================
-
-// 切換主頁籤 (終端機 / 設備查詢 / 設備配置 / 系統設定)
-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' && typeof fitAddon !== 'undefined' && 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');
-}
-
-// 🌟 新增一個變數來記錄上一次的樹狀圖模式
-let lastTreeMode = null;
-
-// 切換「設備配置」內的子任務表單
-function switchConfigTask() {
- document.querySelectorAll('#config-tab .task-form').forEach(form => {
- form.classList.remove('active');
- form.style.display = 'none';
- });
-
- const selectedTaskId = document.getElementById('configTask').value;
-
- let targetFormId = selectedTaskId;
- if (selectedTaskId === 'form-running-config' || selectedTaskId === 'form-full-config') {
- targetFormId = 'form-tree-config';
- }
-
- const targetForm = document.getElementById(targetFormId);
- if (targetForm) {
- targetForm.classList.add('active');
- targetForm.style.display = 'block';
-
- const titleSpan = document.getElementById('tree-form-title');
- const currentMode = selectedTaskId === 'form-full-config' ? 'full' : 'running';
-
- if (titleSpan) {
- titleSpan.textContent = currentMode === 'running'
- ? '🌳 設備配置樹狀圖 (running-config)'
- : '🌳 完整設備配置樹狀圖 (full configuration)';
- }
-
- if (targetFormId === 'form-tree-config') {
- // 🌟 魔法所在:純視覺切換,不銷毀資料!
- const runningContainer = document.getElementById('tree-container-running');
- const fullContainer = document.getElementById('tree-container-full');
- if (runningContainer) runningContainer.style.display = currentMode === 'running' ? 'block' : 'none';
- if (fullContainer) fullContainer.style.display = currentMode === 'full' ? 'block' : 'none';
-
- if (lastTreeMode !== currentMode) {
- const statusEl = document.getElementById('scan-status');
- if (statusEl) statusEl.textContent = '';
-
- // 🌟 新增:如果使用者在載入途中切換模式,強制把沙漏文字隱藏!
- const loadingMsg = document.getElementById('loading-message');
- if (loadingMsg) loadingMsg.style.display = 'none';
-
- lastTreeMode = currentMode;
- }
- }
-
- initGlobalSSE(currentMode);
- }
-}
-
-// 啟動配置任務 (樹狀圖載入 或 MAC Domain 精靈)
-function startConfigTask() {
- if (!isConnected) {
- alert("請先點擊畫面上方的「連線至 CMTS」成功後,再執行載入任務!");
- return;
- }
- switchConfigTask();
- const selectedTaskId = document.getElementById('configTask').value;
-
- if (selectedTaskId === 'form-bonding-config') {
- if (hasMacDomainData() && !confirm("重新載入將會清除您目前未套用的設定,確定要繼續嗎?")) return;
- resetAllManagerData();
- loadMacDomainList();
- }
- // 🌟 修正:正確的括號閉合,並根據下拉選單傳遞 configType
- else if (selectedTaskId === 'form-running-config') {
- fetchFullConfig('running');
- }
- else if (selectedTaskId === 'form-full-config') {
- fetchFullConfig('full'); // 🌟 補上 full 參數
- }
-}
-
-// 切換查詢輸入框的啟用狀態 (全域指令不需輸入目標)
-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 (留白則查詢全體)";
- }
-}
-
-// 重置所有管理介面的資料與 UI 狀態
-function resetAllManagerData() {
- resetMacDomainData();
-
- // 🧹 [修復 Leak] 清空前端樹狀圖快取,避免切換設備時發生 OOM
- clearTreeCache();
-
- // 🛡️ [跨設備防護] 切換設備時,清空鎖定輪詢的記憶體
- if (typeof activeLockState !== 'undefined' && activeLockState.clear) {
- activeLockState.clear();
- }
-
- 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 = '';
-
- // 🌟 新增:切換設備連線後,自動重整備份歷史紀錄
- if (typeof loadBackupHistory === 'function') {
- loadBackupHistory();
- }
-}
-
-// ============================================================================
-// 🌟 3. 設備查詢與全域配置載入 (Query & Full Config)
-// ============================================================================
-
-// 執行綜合查詢 (CM / RPD) - 🌟 導入前端模擬動態訊息流
-async function executeQuery(mode) {
- if (!isConnected) return alert("請先點擊畫面上方的「連線至 CMTS」按鈕!");
-
- 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');
-
- // 🌟 模擬動態訊息流邏輯
- const states = [
- `🔌 正在建立 SSH 連線至 ${connInfo.host}...`,
- `📥 正在向設備發送查詢指令...`,
- `🧩 正在等待設備回傳資料...`
- ];
- let progressIdx = 0;
- outputEl.innerHTML = `
${states[0]}`;
-
- const progressInterval = setInterval(() => {
- progressIdx++;
- if (progressIdx < states.length) {
- const textEl = document.getElementById('query-progress-text');
- if (textEl) textEl.innerText = states[progressIdx];
- }
- }, 1500); // 每 1.5 秒切換一次狀態
-
- try {
- const result = await apiExecuteQuery(queryType, target, connInfo.host, connInfo.user, connInfo.pass);
- clearInterval(progressInterval); // 收到結果,停止輪播
-
- if (result.status === 'success') {
- outputEl.style.color = "#e0e0e0";
- outputEl.textContent = isDebug ? `[DEBUG] 實際發送指令: ${result.command}\n==================================================\n${result.data}` : result.data;
- } else {
- outputEl.style.color = "#e74c3c";
- outputEl.textContent = `查詢失敗:\n${result.detail || result.message}`;
- }
- } catch (error) {
- clearInterval(progressInterval); // 發生錯誤,停止輪播
- outputEl.style.color = "#e74c3c";
- outputEl.textContent = `連線失敗: ${error}`;
- }
-}
-
-// 載入完整設備配置並生成樹狀圖,預設為 running (平順過渡終極版 🚀)
-async function fetchFullConfig(configType = 'running') {
- const loadingMsg = document.getElementById('loading-message');
- const treeContainer = document.getElementById(`tree-container-${configType}`);
-
- const connInfo = getGlobalConnectionInfo();
- if (!connInfo) return;
-
- if (loadingMsg) {
- loadingMsg.style.display = 'inline-block';
- loadingMsg.style.color = '#f39c12'; // 🌟 確保初始狀態為橘色
- loadingMsg.innerHTML = '⏳ 準備連線至設備...';
- }
- if (treeContainer) treeContainer.innerHTML = '';
-
- let needAutoScan = false;
- let progressInterval = null;
-
- try {
- try {
- const versionRes = await apiGetCmtsVersion(connInfo.host, connInfo.user, connInfo.pass);
- if (versionRes.status === 'success') {
- const currentVersion = versionRes.version;
- const optRes = await fetch(`/api/v1/cmts-leaf-options?host=${encodeURIComponent(connInfo.host)}&config_type=${configType}`);
- const optData = await optRes.json();
- const cachedVersion = optData.__metadata__?.cmts_version;
-
- if (cachedVersion && cachedVersion !== currentVersion) {
- const doClear = confirm(`⚠️ 偵測到 CMTS 韌體版本已變更!\n\n設備當前版本:${currentVersion}\n快取紀錄版本:${cachedVersion}\n\n為確保指令正確,系統強烈建議清空舊版快取。是否立即清空?`);
- if (doClear) {
- const allKeys = Object.keys(optData);
- await apiClearCache(connInfo.host, allKeys, configType);
- needAutoScan = true;
- }
- }
- }
- } catch (vErr) {
- console.warn("版本檢查失敗,略過", vErr);
- }
-
- const response = await fetch(`/api/v1/cmts-full-config/stream?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}&config_type=${configType}`);
-
- if (!response.ok) throw new Error(`伺服器回應錯誤: ${response.status}`);
-
- const reader = response.body.getReader();
- const decoder = new TextDecoder("utf-8");
- let buffer = "";
- let finalTreeData = null;
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
-
- buffer += decoder.decode(value, { stream: true });
- let lines = buffer.split('\n');
- buffer = lines.pop();
-
- for (let line of lines) {
- if (!line.trim()) continue;
- try {
- const data = JSON.parse(line);
-
- if (data.status === 'progress') {
- if (data.message.includes('正在下載')) {
- let progress = 0;
- if (progressInterval) clearInterval(progressInterval);
-
- progressInterval = setInterval(() => {
- progress += (95 - progress) * 0.06;
- if (loadingMsg) {
- loadingMsg.innerHTML = `📥 正在下載 ${configType} 配置檔 (${Math.round(progress)}%)...`;
- }
- }, 500);
- } else {
- if (progressInterval) {
- clearInterval(progressInterval);
- progressInterval = null;
- if (loadingMsg) {
- loadingMsg.innerHTML = `📥 正在下載 ${configType} 配置檔 (100%) - 下載完成!`;
- loadingMsg.style.color = "#27ae60";
- }
- await new Promise(resolve => setTimeout(resolve, 600));
- if (loadingMsg) loadingMsg.style.color = "#f39c12"; // 🌟 恢復橘色
- }
- if (loadingMsg) {
- loadingMsg.style.color = "#f39c12"; // 🌟 確保橘色
- loadingMsg.innerHTML = data.message;
- }
- }
- }
- else if (data.status === 'success') {
- finalTreeData = data.data;
- }
- else if (data.status === 'error') {
- throw new Error(data.message);
- }
- } catch (e) {
- console.warn("解析串流 JSON 失敗:", line);
- }
- }
- }
-
- if (!finalTreeData) throw new Error("未收到完整的配置資料,連線可能中斷。");
-
- window.currentTreeData = finalTreeData;
-
- const currentSelectedTask = document.getElementById('configTask').value;
- const expectedTask = configType === 'running' ? 'form-running-config' : 'form-full-config';
-
- if (currentSelectedTask !== expectedTask) return;
-
- if (treeContainer) {
- treeContainer.innerHTML = buildTree(finalTreeData, '', false, configType);
- if (loadingMsg) loadingMsg.style.display = 'none';
-
- const isSomeoneScanning = await apiGetScanStatus(connInfo.host, configType);
- if (isSomeoneScanning) {
- alert("💡 提示:系統正在背景更新設備選項快取,部分選單題示內容可能稍後才會顯示完整。");
- lockScanButton(60000);
- } else {
- enableGlobalScanButton();
- if (needAutoScan) {
- setTimeout(() => scanGlobalMissingOptions(configType), 500);
- }
- }
- }
-
- } catch (error) {
- if (treeContainer) treeContainer.innerHTML = `
❌ 連線或解析失敗: ${error.message}
`;
- } finally {
- if (progressInterval) clearInterval(progressInterval);
- if (loadingMsg) {
- loadingMsg.style.display = 'none';
- loadingMsg.style.color = "#f39c12"; // 🌟 確保重置為橘色
- }
- }
-}
-
-// ============================================================================
-// 🌟 CM 一鍵診斷中心邏輯 (多頻道切換與紅綠燈實作)
-// ============================================================================
-let merChartInstance = null;
-let globalMerData = {}; // 暫存所有頻道的 MER 資料供切換使用
-
-window.runCmDiagnostics = async function() {
- const connInfo = getGlobalConnectionInfo();
- if (!connInfo) return;
-
- const macInput = document.getElementById('diagMacInput').value.trim();
- if (!macInput) return alert("請輸入目標 CM 的 MAC Address!");
-
- const btn = document.getElementById('btnRunDiag');
- const msg = document.getElementById('diagStatusMsg');
- const resultArea = document.getElementById('diagResultArea');
-
- btn.disabled = true;
- btn.style.opacity = '0.7';
- msg.style.display = 'inline-block';
- resultArea.style.display = 'none';
-
- try {
- const result = await apiGetCmDiagnostics(connInfo.host, connInfo.user, connInfo.pass, macInput);
-
- if (result.status === 'success') {
- const data = result.data;
-
- // 1. 填入基本資訊
- document.getElementById('diagResMac').textContent = data.mac;
- document.getElementById('diagResIp').textContent = data.ip || 'N/A';
- document.getElementById('diagResCpe').textContent = data.cpe_count;
-
- const stateEl = document.getElementById('diagResState');
- stateEl.textContent = data.state || 'N/A';
- stateEl.style.color = (data.state && data.state.toLowerCase().includes('online')) ? '#27ae60' : '#e74c3c';
-
- document.getElementById('diagResTx').textContent = data.phy.tx_power !== null ? data.phy.tx_power : 'N/A';
- document.getElementById('diagResRx').textContent = data.phy.rx_power !== null ? data.phy.rx_power : 'N/A';
-
- // 2. 渲染上行 SNR (Upstreams) 標籤群
- const usContainer = document.getElementById('upstreamSnrContainer');
- usContainer.innerHTML = '';
- if (data.upstreams && data.upstreams.length > 0) {
- data.upstreams.forEach(us => {
- let color = '#e74c3c'; // 預設紅 (差: < 30)
- if (us.snr >= 35) color = '#27ae60'; // 綠 (優: >= 35)
- else if (us.snr >= 30) color = '#f39c12'; // 橘 (尚可: 30~34.9)
-
- usContainer.innerHTML += `
-
- ${us.channel} : ${us.snr}
-
- `;
- });
- } else {
- usContainer.innerHTML = '
無資料';
- }
-
- // 3. 處理 OFDM MER 頻道按鈕與圖表
- globalMerData = data.mer_channels;
- const tabsContainer = document.getElementById('ofdmChannelTabs');
- tabsContainer.innerHTML = '';
-
- const channels = Object.keys(globalMerData);
- if (channels.length > 0) {
- channels.forEach((ch, index) => {
- const chData = globalMerData[ch];
- // 根據後端判斷的健康度上色 (>=41綠, 38~40橘, <=37紅)
- let bgCol = '#bdc3c7'; // 預設灰色 (無資料)
- if (chData.health === 'good') bgCol = '#27ae60';
- else if (chData.health === 'warning') bgCol = '#f39c12';
- else if (chData.health === 'critical') bgCol = '#e74c3c';
-
- const btn = document.createElement('button');
- // 💡 拔除方正的 class,改用純手工膠囊樣式
- btn.style.boxSizing = 'border-box';
- btn.style.cursor = 'pointer';
- btn.style.backgroundColor = bgCol;
- btn.style.color = '#ffffff';
- btn.style.padding = '6px 14px';
- btn.style.borderRadius = '20px'; // 🌟 完美的膠囊圓角
- btn.style.fontSize = '12px';
- btn.style.fontWeight = 'bold';
- btn.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
- btn.style.transition = 'all 0.2s ease';
-
- // 點擊與未點擊的狀態區分 (透明度與外框)
- btn.style.opacity = index === 0 ? '1' : '0.5';
- btn.style.border = index === 0 ? '2px solid #2c3e50' : '2px solid transparent';
-
- // 標籤文字加入最低 MER 數值提示
- btn.textContent = `${ch} (Min: ${chData.min_mer} dB)`;
-
- btn.onclick = () => {
- // 重置所有按鈕外觀
- Array.from(tabsContainer.children).forEach(b => {
- b.style.opacity = '0.5';
- b.style.border = '2px solid transparent';
- });
- // 突顯當前點擊的按鈕
- btn.style.opacity = '1';
- btn.style.border = '2px solid #2c3e50';
- // 重新繪圖
- renderMerChart(ch, chData.histogram);
- };
- tabsContainer.appendChild(btn);
- });
-
- // 預設渲染第一個頻道的圖表
- renderMerChart(channels[0], globalMerData[channels[0]].histogram);
- } else {
- tabsContainer.innerHTML = '
⚠️ 設備無回傳任何 OFDM 頻道 MER 數據';
- renderMerChart('無資料', {});
- }
-
- resultArea.style.display = 'flex';
- }
- } catch (error) {
- alert(`❌ 診斷失敗: ${error.message}`);
- } finally {
- btn.disabled = false;
- btn.style.opacity = '1';
- msg.style.display = 'none';
- }
-};
-
-function renderMerChart(channelName, histogramData) {
- if (merChartInstance) {
- merChartInstance.destroy();
- }
- const ctx = document.getElementById('merChart').getContext('2d');
-
- if (!histogramData || Object.keys(histogramData).length === 0) {
- merChartInstance = new Chart(ctx, {
- type: 'bar',
- data: { labels: ['無資料'], datasets: [{ label: 'Subcarriers', data: [0] }] },
- options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: '無法取得 MER 數據' } } }
- });
- return;
- }
-
- const labels = Object.keys(histogramData).sort((a, b) => parseInt(a) - parseInt(b));
- const dataPoints = labels.map(label => histogramData[label]);
-
- merChartInstance = new Chart(ctx, {
- type: 'bar',
- data: {
- labels: labels.map(l => `${l} dB`),
- datasets: [{
- label: 'Subcarriers 數量',
- data: dataPoints,
- backgroundColor: 'rgba(155, 89, 182, 0.6)',
- borderColor: 'rgba(142, 68, 173, 1)',
- borderWidth: 1,
- borderRadius: 4,
- hoverBackgroundColor: 'rgba(155, 89, 182, 0.9)'
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- scales: {
- y: {
- beginAtZero: true,
- title: { display: true, text: 'Subcarriers 數量', font: { weight: 'bold' } },
- grid: { color: '#ecf0f1' }
- },
- x: {
- title: { display: true, text: 'MER (dB)', font: { weight: 'bold' } },
- grid: { display: false }
- }
- },
- plugins: {
- title: {
- display: true,
- text: `顯示中頻道: ${channelName}`,
- font: { size: 15, weight: 'bold' },
- color: '#2c3e50'
- },
- legend: { display: false },
- tooltip: {
- backgroundColor: 'rgba(44, 62, 80, 0.9)',
- titleFont: { size: 14 },
- bodyFont: { size: 14 },
- padding: 10,
- callbacks: {
- label: function(context) {
- return ` ${context.raw} 個 Subcarriers`;
- }
- }
- }
- }
- }
- });
-}
-
-// ============================================================================
-// 🌟 4. 選項快取與背景掃描 (Cache & Background Scanning)
-// ============================================================================
-
-// 為現有的 input 加上下拉選項 (不破壞原有結構),加上 mode 參數
-async function enhanceInputWithOptions(path, elementId, mode = 'running') {
- // 🌟 1. 取得當前連線的 IP
- const connInfo = getGlobalConnectionInfo();
- if (!connInfo || !connInfo.host) return;
-
- try {
- // 🌟 2. GET 請求:網址加上 host 參數
- const optRes = await fetch(`/api/v1/cmts-leaf-options?host=${encodeURIComponent(connInfo.host)}&config_type=${mode}`);
- const optData = await optRes.json();
-
- const cacheKey = path.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
- const leafCache = optData[cacheKey];
-
- if (leafCache && leafCache.options && leafCache.options.length > 0) {
- 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 => `