================================================================================
TARGET SOURCE CODE EXPORT (SPECIFIC)
================================================================================


================================================================================
FILE: static/app.js
================================================================================
/* --- static/app.js --- */

// ============================================================================
// 📦 模組匯入區 (Imports)
// ============================================================================

// 1. 終端機模組 (Terminal)
import { initTerminal, connectWebSocket, disconnectWebSocket, injectCommand, downloadTerminal, isConnected } from './terminal.js';

// 2. API 網路請求模組 (API)
import { 
    apiGetFullConfig, apiClearCache, apiExecuteQuery, 
    apiGetTreeFilters, apiSaveTreeFilters,
    apiSyncLeafOptionsStream, apiGetCmtsVersion,
    apiGetScanStatus, apiGetLockStatus,
    apiGetCmDiagnostics,
    apiListCms, apiListRpds 
} from './api.js';

// 3. 共用工具模組 (Utils)
import { getGlobalConnectionInfo } from './utils.js';

// 4. 樹狀圖 UI 模組 (Tree UI)
import { buildTree, buildRealFilterTree, expandAll, collapseAll, clearTreeCache } from './tree-ui.js';

// 5. 編輯模式與鎖定模組 (Edit Mode)
import { 
    releaseAllLocks, startEditFolder, startEditLeaf, 
    cancelEditFolder, cancelEditLeaf, previewFolderCLI, previewLeafCLI, 
    hideSideCLI, executeSideCLI, previewNodeCLI, SESSION_USER_ID, currentEditElementId  
} 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. 全域生命週期、閒置偵測與 SSE 廣播監聽
// ============================================================================

let idleTimer;
const IDLE_TIMEOUT = 300000; // 5 分鐘 (毫秒)
let globalSSE = null;

// 初始化全域 SSE 監聽器，加上 mode 參數，並在連線前關閉舊頻道
function initGlobalSSE(mode = 'running') {
    const connInfo = getGlobalConnectionInfo();
    if (!connInfo || !connInfo.host) return; // 🌟 確保有連線才建立 SSE
    if (globalSSE) globalSSE.close(); 
    globalSSE = new EventSource(`/api/v1/cmts-leaf-options/stream?host=${encodeURIComponent(connInfo.host)}&config_type=${mode}`); 
    
    globalSSE.onmessage = (event) => {
        const data = JSON.parse(event.data);
        const statusEl = document.getElementById('scan-status');
        
        // 🌟 關鍵修復：只要收到「開始」或「進度」訊號，一律強制鎖定按鈕！(確保中途加入的使用者也會被鎖定)
        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 = `⏳ 背景掃描進度：<b>${data.current} / ${data.total}</b> (正在處理: ${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 = '<option value="">請先點擊上方載入任務...</option>';
    
    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 = `<span id="query-progress-text" style="color: #f39c12; font-weight: bold;">${states[0]}</span>`;
    
    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 = `<div style="color: #c0392b; font-weight: bold; margin: 20px;">❌ 連線或解析失敗: ${error.message}</div>`;
    } 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 += `
                        <div style="background-color: ${color}; color: white; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: bold; box-shadow: 0 1px 3px rgba(0,0,0,0.2);">
                            ${us.channel} : ${us.snr}
                        </div>
                    `;
                });
            } else {
                usContainer.innerHTML = '<span style="color: #bdc3c7; font-size: 13px;">無資料</span>';
            }

            // 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 = '<span style="color: #e74c3c; font-size: 14px; font-weight: bold;">⚠️ 設備無回傳任何 OFDM 頻道 MER 數據</span>';
                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 => `<option value="${opt}">`).join('');
            }
        } else {
            // 🌟 3. POST 請求：把 host 塞進 body 裡面
            fetch('/api/v1/cmts-leaf-options/sync', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ host: connInfo.host, leaf_paths: [cacheKey], config_type: mode })
            });
        }
    } catch (e) {
        console.warn("選項外掛載入失敗，維持純文字輸入", e);
    }
}

// ============================================================================
// 🌟 核心功能：執行掃描 (廣播升級版)，加上 mode 參數
// ============================================================================

// 🌟 新增：遞迴提取所有快取 Key 的輔助函數 (不依賴網頁 DOM)
function extractAllCacheKeys(node, path = '') {
    let keys = [];
    for (const [key, value] of Object.entries(node)) {
        const currentPath = path ? `${path}::${key}` : key;
        if (typeof value === 'object' && value !== null) {
            keys = keys.concat(extractAllCacheKeys(value, currentPath));
        } else {
            const origVal = (value === null ? '' : value).toString();
            let cacheKey = currentPath.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
            if (currentPath.endsWith('::no')) {
                cacheKey = cacheKey.replace(/ no$/, '') + ' ' + origVal;
            }
            keys.push(cacheKey);
        }
    }
    return keys;
}

async function performScan(isGlobal, mode = 'running') {
    const connInfo = getGlobalConnectionInfo();
    if (!connInfo || !connInfo.host) return alert("請先連線至設備！");

    const isSomeoneScanning = await apiGetScanStatus(connInfo.host, mode);
    if (isSomeoneScanning) {
        alert("目前已有其他使用者正在執行掃描，請稍候共享更新結果！");
        return;
    }
    
    const treeContainer = document.getElementById(`tree-container-${mode}`);
    if (!treeContainer) return;

    const statusEl = document.getElementById('scan-status');
    
    try {
        const optRes = await fetch(`/api/v1/cmts-leaf-options?host=${encodeURIComponent(connInfo.host)}&config_type=${mode}`);
        const optData = await optRes.json();
        const pathsToSync = [];

        if (isGlobal) {
            // 🌟 全域掃描：直接從原始 JSON 資料遞迴提取，不依賴 DOM
            if (!window.currentTreeData) return alert("請先載入設備配置！");
            const allKeys = extractAllCacheKeys(window.currentTreeData);
            
            allKeys.forEach(cacheKey => {
                if (!optData[cacheKey] && !pathsToSync.includes(cacheKey)) {
                    pathsToSync.push(cacheKey);
                }
            });
        } else {
            // 🌟 局部掃描：維持原樣，只抓畫面上看得到的 (已展開的)
            const allContainers = treeContainer.querySelectorAll('.leaf-container');
            allContainers.forEach(container => {
                const isHiddenByFolder = container.closest('details:not([open])') !== null;
                if (!isHiddenByFolder) {
                    const path = container.getAttribute('data-path');
                    if (path) {
                        const origVal = container.getAttribute('data-original') || '';
                        let cacheKey = path.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
                        if (path.endsWith('::no')) cacheKey = cacheKey.replace(/ no$/, '') + ' ' + origVal;
                        
                        if (!optData[cacheKey] && !pathsToSync.includes(cacheKey)) {
                            pathsToSync.push(cacheKey);
                        }
                    }
                }
            });
        }

       if (pathsToSync.length === 0) {
            statusEl.textContent = isGlobal ? "✅ 全域所有欄位都已有最新選項，無需掃描！" : "✅ 局部展開的欄位都已有最新選項，無需掃描！";
            statusEl.style.color = "#27ae60";
            setTimeout(() => statusEl.textContent = "", 3000);
            return;
        }

        const response = await fetch('/api/v1/cmts-leaf-options/sync', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ host: connInfo.host, leaf_paths: pathsToSync, config_type: mode })
        });
        
        const result = await response.json();
        if (result.status === 'busy') {
            alert(result.message);
        }

    } catch (error) {
        console.error("掃描請求發送失敗:", error);
        statusEl.textContent = "❌ 掃描請求發送失敗，請檢查網路連線。";
        statusEl.style.color = "#e74c3c";
    }
}

// ============================================================================
// 🌟 核心功能：清除快取 (廣播升級版)，加上 mode 參數
// ============================================================================
async function performClear(isGlobal, mode = 'running') {
    // 🌟 1. 取得當前連線的 IP
    const connInfo = getGlobalConnectionInfo();
    if (!connInfo || !connInfo.host) return alert("請先連線至設備！");

    // 🌟 2. 檢查掃描狀態時，傳入 host
    const isSomeoneScanning = await apiGetScanStatus(connInfo.host, mode);
    if (isSomeoneScanning) {
        alert("⚠️ 系統正在進行全域選項掃描更新中！\n為避免資料衝突，請稍候掃描完成再執行清除動作。");
        return;
    }

    let pathsToClear = [];
    
    if (isGlobal) {
        try {
            // 🌟 3. GET 請求：網址加上 host 參數
            const optRes = await fetch(`/api/v1/cmts-leaf-options?host=${encodeURIComponent(connInfo.host)}&config_type=${mode}`);
            const optData = await optRes.json();
            pathsToClear = Object.keys(optData).filter(k => k !== '__metadata__');
        } catch (e) {
            console.error(e);
            return alert("❌ 無法取得全域快取清單。");
        }
    } else {
        const treeContainer = document.getElementById(`tree-container-${mode}`);
        const elements = treeContainer ? treeContainer.querySelectorAll('.leaf-container') : []; 
        elements.forEach(el => {
            const isHiddenByFolder = el.closest('details:not([open])') !== null;
            if (!isHiddenByFolder) { 
                const path = el.getAttribute('data-path');
                if (path) {
                    const cacheKey = path.replace(/::/g, ' ');
                    if (!pathsToClear.includes(cacheKey)) pathsToClear.push(cacheKey);
                }
            }
        });
    }

    if (pathsToClear.length === 0) return alert(isGlobal ? "目前沒有任何快取可以清除！" : "局部沒有展開的選項可以清除！\n請先展開您想重抓的資料夾。");
    
    const msg = isGlobal 
        ? `確定要清除「全域」共 ${pathsToClear.length} 個選項的快取嗎？\n(這將會清空所有已儲存的下拉選單資料)` 
        : `確定要清除局部 ${pathsToClear.length} 個選項的快取嗎？\n(清除後將自動為您重新掃描)`;
    
    if (!confirm(msg)) return;

    try {
        // 🌟 4. 呼叫清除 API 時，傳入 host
        const result = await apiClearCache(connInfo.host, pathsToClear, mode); 
        const statusEl = document.getElementById('scan-status');
        if (statusEl) {
            statusEl.textContent = `✅ 成功清除 ${result.cleared_count} 筆快取！準備重新掃描...`;
            statusEl.style.color = "#27ae60";
        }
        
        // 清除完畢後，自動呼叫簡化版的 performScan
        await performScan(isGlobal, mode);
        
    } catch (error) {
        console.error("清除快取失敗:", error);
        alert("❌ 清除快取失敗，請檢查網路連線或後端 API 是否正確啟動。");
    }
}

// 綁定給 HTML 呼叫的 4 個獨立函數
function scanVisibleMissingOptions() { 
    performScan(false); 
}
function scanGlobalMissingOptions() { 
    // 🌟 動態抓取當前選擇的模式，傳給 performScan
    const mode = document.getElementById('configTask').value === 'form-full-config' ? 'full' : 'running';
    performScan(true, mode); 
}
function clearVisibleCache() { 
    performClear(false); 
}
function clearGlobalCache() { 
    // 🌟 動態抓取當前選擇的模式，傳給 performClear
    const mode = document.getElementById('configTask').value === 'form-full-config' ? 'full' : 'running';
    performClear(true, mode); 
}

// ============================================================================
// 🌟 5. 系統設定與過濾器 (System Settings & Filters)
// ============================================================================

let currentHiddenKeys = [];

// 🌟 新增：切換過濾器模式時觸發
async function switchFilterMode() {
    const container = document.getElementById('filter-checkboxes');
    if (container) container.style.display = 'none'; // 先隱藏樹狀圖，強迫使用者重新載入
    await openSystemSettings();
}

// 開啟系統設定並讀取目前的過濾器清單
async function openSystemSettings() {
    // 🌟 動態抓取過濾器模式
    const modeSelect = document.getElementById('filter-mode-select');
    const mode = modeSelect ? modeSelect.value : 'running';

    try {
        const result = await apiGetTreeFilters(mode); 
        if (result.status === 'success') currentHiddenKeys = result.data;
    } catch (error) {
        console.error("讀取設定失敗:", error);
    }
}

// 載入真實設備配置以供過濾器勾選 - 🌟 升級為真實動態訊息流 (重用現有串流 API)
async function loadRealConfigForFilters() {
    const connInfo = getGlobalConnectionInfo();
    if (!connInfo) return alert("請先在畫面上方輸入設備連線資訊！");

    const modeSelect = document.getElementById('filter-mode-select');
    const mode = modeSelect ? modeSelect.value : 'running';

    const loadingMsg = document.getElementById('filter-loading-msg');
    const container = document.getElementById('filter-checkboxes');
    
    loadingMsg.style.display = 'inline-block';
    loadingMsg.style.color = '#f39c12'; // 🌟 確保橘色
    loadingMsg.innerHTML = '⏳ 準備連線至設備...';
    container.style.display = 'none';

    let progressInterval = null; // 🌟 新增：進度條計時器

    try {
        const url = `/api/v1/cmts-full-config/stream?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}&skip_filter=true&config_type=${mode}`;
        const response = await fetch(url);
        
        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 = `📥 正在下載 ${mode} 配置檔 (${Math.round(progress)}%)...`;
                                }
                            }, 500);
                        } else {
                            if (progressInterval) {
                                clearInterval(progressInterval);
                                progressInterval = null;
                                if (loadingMsg) {
                                    loadingMsg.innerHTML = `📥 正在下載 ${mode} 配置檔 (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) {
            container.style.display = 'block';
            container.innerHTML = buildRealFilterTree(finalTreeData, "", currentHiddenKeys, false, mode);
        } else {
            throw new Error("未收到完整的配置資料");
        }

    } catch (error) {
        container.style.display = 'block';
        container.innerHTML = `<div style="color: #c0392b; font-weight: bold;">❌ 連線或解析失敗: ${error.message}</div>`;
    } finally {
        if (progressInterval) clearInterval(progressInterval); // 🌟 確保清除計時器
        loadingMsg.style.display = 'none';
    }
}

// 當點擊「父節點」時：同步勾選/取消所有子節點
function toggleChildCheckboxes(parentCheckbox) {
    const details = parentCheckbox.closest('details');
    if (details) {
        const childCheckboxes = details.querySelectorAll('.filter-children-container .filter-checkbox');
        childCheckboxes.forEach(cb => cb.checked = parentCheckbox.checked);
    }
    updateParentCheckboxState(parentCheckbox);
}

// 當點擊「子節點」時：往上檢查父節點是否該被勾選
function updateParentCheckboxState(element) {
    const parentDetails = element.closest('.filter-children-container')?.closest('details');
    if (!parentDetails) return;

    const parentCheckbox = parentDetails.querySelector('summary .filter-checkbox');
    const childCheckboxes = parentDetails.querySelectorAll('.filter-children-container .filter-checkbox');
    
    if (parentCheckbox && childCheckboxes.length > 0) {
        const allChecked = Array.from(childCheckboxes).every(cb => cb.checked);
        parentCheckbox.checked = allChecked;
        updateParentCheckboxState(parentCheckbox);
    }
}

// 儲存系統設定 (過濾器)
async function saveSystemSettings() {
    // 🌟 動態抓取過濾器模式
    const modeSelect = document.getElementById('filter-mode-select');
    const mode = modeSelect ? modeSelect.value : 'running';

    const checkboxes = document.querySelectorAll('.filter-checkbox:checked');
    const selectedHiddenKeys = Array.from(checkboxes).map(cb => cb.value);
    
    try {
        const result = await apiSaveTreeFilters(selectedHiddenKeys, mode); 
        if (result.status === 'success') {
            const statusText = document.getElementById('settings-status');
            statusText.style.display = 'inline-block';
            currentHiddenKeys = selectedHiddenKeys; 
            setTimeout(() => { statusText.style.display = 'none'; }, 3000);
        }
    } catch (error) {
        console.error("儲存設定失敗:", error);
        alert("儲存設定時發生錯誤！");
    }
}

// ============================================================================
// 🌟 新增：設備備份與還原 (Backup & Restore)
// ============================================================================

// 🌟 新增：全域變數用來暫存所有快照資料，實現前端秒速過濾
let allBackupsData = [];

// 1. 建立新快照 - 🌟 升級為真實動態訊息流 (含百分比動畫)
async function createSnapshot() {
    if (!isConnected) {
        return alert("⚠️ 請先點擊畫面上方的「連線至 CMTS」成功後，再執行備份任務！\n(確保您清楚目前正在操作哪一台設備)");
    }

    const connInfo = getGlobalConnectionInfo();
    if (!connInfo || !connInfo.host) return;

    const snapshotName = document.getElementById('snapshotName').value.trim();
    if (!snapshotName) return alert("請輸入快照名稱！");

    const snapshotDescription = document.getElementById('snapshotDescription')?.value.trim() || "";
    const configType = document.getElementById('backupConfigType').value;
    const btn = document.getElementById('btnCreateSnapshot');
    const msg = document.getElementById('backup-status-msg');

    btn.disabled = true;
    msg.style.display = 'inline-block';
    msg.style.color = '#f39c12'; // 🌟 確保橘色
    msg.innerHTML = '⏳ 準備連線至設備...';

    let progressInterval = null; // 🌟 新增：進度條計時器

    try {
        const response = await fetch('/api/v1/backups/snapshot', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                host: connInfo.host,
                username: connInfo.user,
                password: connInfo.pass,
                snapshot_name: snapshotName,
                description: snapshotDescription,
                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 isSuccess = false;
        let finalMessage = "";

        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 (msg) {
                                    msg.innerHTML = `📥 正在下載 ${configType} 配置檔 (${Math.round(progress)}%)...`;
                                }
                            }, 500);
                        } else {
                            if (progressInterval) {
                                clearInterval(progressInterval);
                                progressInterval = null;
                                if (msg) {
                                    msg.innerHTML = `📥 正在下載 ${configType} 配置檔 (100%) - 下載完成！`;
                                    msg.style.color = "#27ae60"; // 瞬間變綠色
                                }
                                await new Promise(resolve => setTimeout(resolve, 600));
                                if (msg) msg.style.color = "#f39c12"; // 恢復橘色
                            }
                            if (msg) {
                                msg.style.color = '#f39c12'; 
                                msg.innerHTML = data.message;
                            }
                        }
                    } else if (data.status === 'success') {
                        isSuccess = true;
                        finalMessage = data.message;
                    } else if (data.status === 'error') {
                        throw new Error(data.message);
                    }
                } catch (e) {
                    if (e.message) throw e; 
                    console.warn("解析串流 JSON 失敗:", line);
                }
            }
        }

        if (isSuccess) {
            alert(`✅ ${finalMessage}`);
            document.getElementById('snapshotName').value = '';
            if (document.getElementById('snapshotDescription')) {
                document.getElementById('snapshotDescription').value = '';
            }
            loadBackupHistory();
        }
    } catch (error) {
        alert(`❌ 發生錯誤: ${error.message}`);
    } finally {
        if (progressInterval) clearInterval(progressInterval); // 🌟 確保清除計時器
        btn.disabled = false;
        msg.style.display = 'none';
    }
}

// 2. 載入歷史紀錄列表 (維持 IP 綁定，避免後端報錯)
async function loadBackupHistory() {
    // 🌟 補回取得當前畫面上設定的 IP
    const connInfo = getGlobalConnectionInfo();
    if (!connInfo || !connInfo.host) return;

    const tbody = document.getElementById('backup-history-tbody');
    if (!tbody) return;
    
    // 🌟 修正：將原本的灰色改為橘色並加粗
    tbody.innerHTML = '<tr><td colspan="5" style="padding: 20px; text-align: center; color: #f39c12; font-weight: bold;">⏳ 正在載入歷史紀錄...</td></tr>';

    try {
        // 🌟 關鍵修改：將 config_type 改為 'all'，一次把該設備的所有快照撈回來
        const response = await fetch(`/api/v1/backups/?host=${encodeURIComponent(connInfo.host)}&config_type=all`);
        const result = await response.json();

        if (result.status === 'success') {
            allBackupsData = result.data;
            applyBackupFilters(); // 載入後立刻套用目前的過濾條件進行渲染
        } else {
            // 加入防呆：如果後端回傳 422 (detail)，也能正確印出錯誤而不是 undefined
            const errMsg = result.message || (result.detail ? JSON.stringify(result.detail) : '未知錯誤');
            tbody.innerHTML = `<tr><td colspan="5" style="padding: 20px; text-align: center; color: #e74c3c;">❌ 載入失敗: ${errMsg}</td></tr>`;
        }
    } catch (error) {
        tbody.innerHTML = `<tr><td colspan="5" style="padding: 20px; text-align: center; color: #e74c3c;">❌ 連線失敗: ${error.message}</td></tr>`;
    }
}

// 💡 前端多維度過濾器邏輯
window.applyBackupFilters = function() {
    const tbody = document.getElementById('backup-history-tbody');
    if (!tbody) return;

    // 取得所有過濾條件 (加上 trim() 避免不小心多敲了空白鍵)
    const keyword = (document.getElementById('filter-keyword')?.value || '').trim().toLowerCase();
    const type = document.getElementById('filter-type')?.value || '';
    const dateStart = document.getElementById('filter-date-start')?.value;
    const dateEnd = document.getElementById('filter-date-end')?.value;

    // 進行陣列過濾
    const filteredData = allBackupsData.filter(item => {
        // 💡 終極防呆版：確保變數存在，並同時搜尋名稱與描述
        const safeName = (item.snapshot_name || '').toLowerCase();
        const safeDesc = (item.description || '').toLowerCase();
        const matchKeyword = safeName.includes(keyword) || safeDesc.includes(keyword);
        
        const matchType = type === '' || item.config_type === type;
        
        let matchDate = true;
        const itemDate = new Date(item.timestamp);
        if (dateStart) {
            const start = new Date(dateStart);
            start.setHours(0, 0, 0, 0);
            if (itemDate < start) matchDate = false;
        }
        if (dateEnd) {
            const end = new Date(dateEnd);
            end.setHours(23, 59, 59, 999);
            if (itemDate > end) matchDate = false;
        }

        return matchKeyword && matchType && matchDate;
    });

    // 渲染過濾後的結果
    if (filteredData.length > 0) {
        tbody.innerHTML = filteredData.map(item => `
            <tr style="border-bottom: 1px solid #ecf0f1; transition: background-color 0.2s;" onmouseover="this.style.backgroundColor='#fcfcfc'" onmouseout="this.style.backgroundColor='transparent'">
                <td style="padding: 12px 10px; color: #7f8c8d; font-size: 14px;">${new Date(item.timestamp).toLocaleString()}</td>
                <td style="padding: 12px 10px; font-weight: bold; color: #2c3e50;">${item.snapshot_name}</td>
                <!-- 🟢 新增描述欄位，加上防撐破設計與 title 提示 -->
                <td style="padding: 12px 10px; color: #7f8c8d; max-width: 250px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${item.description || ''}">
                    ${item.description || '<span style="color: #bdc3c7; font-style: italic;">無</span>'}
                </td>
                <td style="padding: 12px 10px; color: #34495e;"><span style="background: #e8f8f5; color: #27ae60; padding: 3px 8px; border-radius: 12px; font-size: 12px;">${item.config_type}</span></td>
                <td style="padding: 12px 10px; text-align: right;">
                    <button onclick="viewBackup('${item.id}')" class="btn-modern btn-scan" style="padding: 4px 8px; font-size: 12px; margin-right: 5px;">👁️ 檢視</button>
                    <button onclick="restoreBackup('${item.id}', '${item.snapshot_name}', '${item.host}')" class="btn-modern btn-save" style="padding: 4px 8px; font-size: 12px; margin-right: 5px; background-color: #8e44ad;" title="將設備還原至此狀態">⚡ 還原</button>
                    <button onclick="deleteBackup('${item.id}')" class="btn-modern btn-disconnect" style="padding: 4px 8px; font-size: 12px;">🗑️ 刪除</button>
                </td>
            </tr>
        `).join('');
    } else {
        // 🟢 修正：沒有資料時，colspan 改為 5
        tbody.innerHTML = '<tr><td colspan="5" style="padding: 20px; text-align: center; color: #7f8c8d;">找不到符合條件的備份紀錄。</td></tr>';
    }
};

// 3. 檢視特定快照內容
async function viewBackup(backupId) {
    openModal("載入快照中...");
    const outputEl = document.getElementById('modalOutput');
    outputEl.innerHTML = `<span style="color: #f39c12;">⏳ 正在向資料庫撈取快照資料，請稍候...</span>`;

    try {
        const response = await fetch(`/api/v1/backups/${backupId}`);
        const result = await response.json();

        if (result.status === 'success') {
            // 確保這行真的有加進去，且檔案有按下 Ctrl+S 儲存！
            const data = result.data; 

            // 🟤 將描述加入彈出視窗的標頭中
            const descText = data.description ? data.description : '無';
            const infoHeader = `【快照名稱】: ${data.snapshot_name}\n【備份描述】: ${descText}\n【建立時間】: ${new Date(data.timestamp).toLocaleString()}\n【設備 IP】: ${data.host}\n==================================================\n\n`;
            
            // 如果後端有回傳 raw_cli 就優先顯示，否則將 parsed_tree 轉為字串顯示
            const content = data.raw_cli ? data.raw_cli : JSON.stringify(data.parsed_tree, null, 2);
            
            outputEl.style.color = "#e0e0e0";
            outputEl.textContent = infoHeader + content;
        } else {
            outputEl.style.color = "#e74c3c";
            outputEl.textContent = `❌ 讀取失敗: ${result.message}`;
        }
    } catch (error) {
        outputEl.style.color = "#e74c3c";
        outputEl.textContent = `❌ 連線失敗: ${error.message}`;
    }
}

// 4. 刪除快照
async function deleteBackup(backupId) {
    if (!confirm("⚠️ 確定要永久刪除這筆備份紀錄嗎？此動作無法復原！")) return;

    try {
        const response = await fetch(`/api/v1/backups/${backupId}`, { method: 'DELETE' });
        const result = await response.json();

        if (result.status === 'success') {
            loadBackupHistory(); // 刪除成功後重新整理列表
        } else {
            alert(`❌ 刪除失敗: ${result.message}`);
        }
    } catch (error) {
        alert(`❌ 發生錯誤: ${error.message}`);
    }
}

// 5. 請求設備差異分析 (安全還原第一步)
// 🌟 接收第三個參數 backupHost
async function restoreBackup(snapshotId, snapshotName, backupHost) {
    // 🛡️ 防線一：必須先連線
    if (!isConnected) {
        return alert("⚠️ 請先點擊畫面上方的「連線至 CMTS」成功後，再執行還原任務！");
    }

    const connInfo = getGlobalConnectionInfo();
    if (!connInfo || !connInfo.host) return;

    // 🛡️ 防線二：嚴格阻斷跨設備還原
    if (backupHost && backupHost !== connInfo.host) {
        alert(
            `🚨 【跨設備還原阻斷】🚨\n\n` +
            `這份快照屬於設備：${backupHost}\n` +
            `您目前連線的設備：${connInfo.host}\n\n` +
            `⚠️ 系統目前禁止跨設備還原，以防止嚴重的網路衝突。請先連線至正確的設備再執行此操作！`
        );
        return; // 直接中斷，不進行後續 API 呼叫
    }

    const modal = document.getElementById('outputModal');
    const modalOutput = document.getElementById('modalOutput');
    const modalTargetInfo = document.getElementById('modalTargetInfo');
    
    // 1. 移除進度條，改為純文字百分比動態顯示
    modalTargetInfo.innerText = `正在分析快照差異: ${snapshotName}`;
    // 🌟 修正：將原本的藍色改為橘色
    modalOutput.innerHTML = `
        <div id="diff-progress-text" style="color: #f39c12; margin-bottom: 10px; font-weight: bold; font-size: 15px;">🔍 正在抓取設備當前配置，並與快照進行深度比對... (0%)</div>
    `;
    modal.classList.add('active');

    // 啟動漸進式模擬進度邏輯 (最高停在 95%)
    let progress = 0;
    const progressInterval = setInterval(() => {
        progress += (95 - progress) * 0.08; 
        const text = document.getElementById('diff-progress-text');
        if (text) {
            text.innerText = `🔍 正在抓取設備當前配置，並與快照進行深度比對... (${Math.round(progress)}%)`;
        }
    }, 400);

    try {
        const response = await fetch(`/api/v1/backups/${snapshotId}/diff`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ host: connInfo.host, username: connInfo.user, password: connInfo.pass })
        });

        const result = await response.json();
        
        // API 回傳後，清除計時器並將進度填滿 100%
        clearInterval(progressInterval);
        const text = document.getElementById('diff-progress-text');
        if (text) {
            text.innerText = `🔍 正在抓取設備當前配置，並與快照進行深度比對... (100%) - 分析完成！`;
        }

        // 稍微延遲 0.5 秒讓使用者看清楚 100%，再渲染結果
        setTimeout(() => {
            if (result.status === 'success') {
                const commands = result.data.commands;
                let diffHtml = `<div style="margin-bottom: 15px; color: #ecf0f1;">以下是將設備還原至 <b>${snapshotName}</b> 所需執行的指令清單：</div>`;
                
                if (commands.length === 0) {
                    diffHtml += `<div style="color: #27ae60; font-weight: bold; padding: 10px; background: rgba(39, 174, 96, 0.1); border-radius: 5px;">✅ 設備當前狀態與快照完全一致，無需進行任何還原動作！</div>`;
                } else {
                    const safeCommands = JSON.stringify(commands).replace(/"/g, '&quot;');

                    // 2. 更改按鈕顏色：使用亮藍色 (#2980b9) 並加上陰影與邊框，凸顯立體感
                    modalTargetInfo.innerHTML = `
                        正在分析快照差異: ${snapshotName}
                        <span style="display: inline-flex; gap: 8px; margin-left: 35px; vertical-align: middle;">
                            <button id="btn-view-diff" onclick="document.getElementById('view-diff').style.display='block'; document.getElementById('view-cli').style.display='none';" class="btn-modern" style="padding: 4px 10px; background: #2980b9; font-size: 13px; border-radius: 4px; color: white; border: 1px solid #2471a3; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.2); transition: all 0.2s;">📊 邏輯差異</button>
                            <button id="btn-view-cli" onclick="document.getElementById('view-diff').style.display='none'; document.getElementById('view-cli').style.display='block';" class="btn-modern" style="padding: 4px 10px; background: #2980b9; font-size: 13px; border-radius: 4px; color: white; border: 1px solid #2471a3; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.2); transition: all 0.2s;">💻 原始執行指令 (Raw CLI)</button>
                        </span>
                    `;

                    // 動態插入確認按鈕到 modal-action-container
                    const actionContainer = document.getElementById('modal-action-container');
                    if (actionContainer) {
                        const oldConfirmBtn = document.getElementById('btn-confirm-restore');
                        if (oldConfirmBtn) oldConfirmBtn.remove();
                        
                        const confirmBtn = document.createElement('button');
                        confirmBtn.id = 'btn-confirm-restore';
                        confirmBtn.className = 'btn-modern btn-save';
                        confirmBtn.style.backgroundColor = '#e74c3c';
                        confirmBtn.style.padding = '6px 12px';
                        confirmBtn.style.fontSize = '13px';
                        confirmBtn.innerHTML = '⚠️ 確認無誤，立即寫入設備並 Commit';
                        confirmBtn.onclick = () => executeSmartRestore(snapshotId, snapshotName, commands);
                        
                        actionContainer.insertBefore(confirmBtn, document.getElementById('btn-modal-close'));
                    }

                    diffHtml += `<pre id="view-diff" style="background: #1e1e1e; padding: 15px; border-radius: 5px; font-family: monospace; overflow-x: auto; display: block;">`;
                    commands.forEach(cmd => {
                        if (cmd.startsWith('no ')) diffHtml += `<span style="color: #e74c3c;">- ${cmd}</span>\n`;
                        else diffHtml += `<span style="color: #2ecc71;">+ ${cmd}</span>\n`;
                    });
                    diffHtml += `</pre>`;

                    diffHtml += `<pre id="view-cli" style="background: #1e1e1e; padding: 15px; border-radius: 5px; font-family: monospace; overflow-x: auto; display: none; color: #ecf0f1;">`;
                    commands.forEach(cmd => { diffHtml += `${cmd}\n`; });
                    diffHtml += `<span style="color: #f39c12;">commit</span>\n`;
                    diffHtml += `</pre>`;
                }
                modalOutput.innerHTML = diffHtml;
            } else {
                modalOutput.innerHTML = `<span style="color: #e74c3c; font-weight: bold;">❌ 差異分析失敗：\n${result.message}</span>`;
            }
        }, 500);

    } catch (error) {
        clearInterval(progressInterval);
        modalOutput.innerHTML = `<span style="color: #e74c3c; font-weight: bold;">❌ 系統錯誤：無法連線至後端伺服器。\n${error.message}</span>`;
    }
}

// 6. 真正執行差異還原 (串接後端 Streaming API)
window.executeSmartRestore = async function(snapshotId, snapshotName, commands) {
    if (!confirm(`⚠️ 警告：即將將 ${commands.length} 條指令寫入設備並 Commit！\n確定要繼續嗎？`)) return;

    // 1. 優雅地將按鈕反灰 (Disabled)，包含關閉按鈕
    const btnIds = ['btn-view-diff', 'btn-view-cli', 'btn-confirm-restore', 'btn-modal-close'];
    btnIds.forEach(id => {
        const btn = document.getElementById(id);
        if (btn) {
            btn.disabled = true;
            btn.style.opacity = '0.5';
            btn.style.cursor = 'not-allowed';
            btn.style.pointerEvents = 'none';
            if (id === 'btn-confirm-restore') {
                btn.innerText = '⏳ 正在寫入設備中...';
                btn.style.backgroundColor = '#7f8c8d';
            }
        }
    });

    const connInfo = getGlobalConnectionInfo();
    const modalOutput = document.getElementById('modalOutput');
    
    // 2. 建立即時 Log 視窗 UI (極致滿版，交由外層 modal-body 控制捲軸)
    modalOutput.innerHTML = `
        <div id="restore-log-container" style="width: 100%; height: 100%; background: transparent; border: none; padding: 0; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; color: #ecf0f1; font-size: 14px; line-height: 1.5; text-align: left; box-sizing: border-box;">
            <div style="color: #f39c12; font-weight: bold; margin-bottom: 8px;">[系統] ⏳ 正在透過 SSH 寫入還原指令，請勿關閉視窗...</div>
            <div style="color: #7f8c8d;">[系統] 準備連線至設備 ${connInfo.host}...</div>
        </div>
    `;
    const logContainer = document.getElementById('restore-log-container');

    try {
        const response = await fetch(`/api/v1/backups/${snapshotId}/restore`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ host: connInfo.host, username: connInfo.user, password: connInfo.pass, commands: commands })
        });

        const reader = response.body.getReader();
        const decoder = new TextDecoder("utf-8");
        let buffer = "";

        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);
                    const timeStr = new Date().toLocaleTimeString('en-US', { hour12: false });
                    
                    if (data.status === 'progress') {
                        logContainer.innerHTML += `<div style="margin-top: 4px;"><span style="color: #3498db;">[${timeStr}]</span> ${data.message}</div>`;
                    } else if (data.status === 'success') {
                        let cleanOutput = data.output || '無';
                        cleanOutput = cleanOutput.replace(/^commit\r?\n/i, '').trim();

                        logContainer.innerHTML += `
                            <div style="margin-top: 15px; color: #2ecc71; font-weight: bold; font-size: 15px;">[${timeStr}] ${data.message}</div>
                            <div style="color: #ecf0f1; margin-top: 5px; border-top: 1px dashed #555; padding-top: 5px;">設備 Commit 回傳：<br><span style="color: #bdc3c7;">${cleanOutput}</span></div>
                        `;
                        document.getElementById('modalTargetInfo').innerHTML = `✅ 還原完成: ${snapshotName}`;
                        
                        // 成功後，隱藏確認按鈕，並恢復關閉按鈕
                        const confirmBtn = document.getElementById('btn-confirm-restore');
                        if (confirmBtn) confirmBtn.style.display = 'none';
                        
                        const closeBtn = document.getElementById('btn-modal-close');
                        if (closeBtn) {
                            closeBtn.disabled = false;
                            closeBtn.style.opacity = '1';
                            closeBtn.style.cursor = 'pointer';
                            closeBtn.style.pointerEvents = 'auto';
                        }
                    } else if (data.status === 'error') {
                        logContainer.innerHTML += `<div style="margin-top: 10px; color: #e74c3c; font-weight: bold;">[${timeStr}] ${data.message}</div>`;
                        if (data.output) {
                            logContainer.innerHTML += `<div style="color: #c0392b; margin-left: 10px; border-left: 2px solid #c0392b; padding-left: 8px;">設備回傳: ${data.output}</div>`;
                        }
                        restoreButtonsState(btnIds);
                    }
                    
                    logContainer.scrollTop = logContainer.scrollHeight;
                } catch (e) {
                    console.warn("解析串流 JSON 失敗:", line);
                }
            }
        }
    } catch (error) {
        logContainer.innerHTML += `<div style="margin-top: 10px; color: #e74c3c; font-weight: bold;">❌ 系統錯誤：無法連線至後端伺服器。<br>${error.message}</div>`;
        restoreButtonsState(btnIds);
    }
};

// 輔助函數：恢復按鈕狀態 (請放在 executeSmartRestore 下方)
function restoreButtonsState(btnIds) {
    btnIds.forEach(id => {
        const btn = document.getElementById(id);
        if (btn) {
            btn.disabled = false;
            btn.style.opacity = '1';
            btn.style.cursor = 'pointer';
            btn.style.pointerEvents = 'auto';
            if (id === 'btn-confirm-restore') {
                btn.innerText = '⚠️ 確認無誤，重新嘗試寫入';
                btn.style.backgroundColor = '#e74c3c';
            }
        }
    });
}

// ============================================================================
// 🌟 6. 共用 UI 元件與狀態管理 (Modals & Button States)
// ============================================================================

// 開啟共用輸出彈出視窗
function openModal(targetInfo) {
    document.getElementById('outputModal').classList.add('active');
    document.getElementById('modalTargetInfo').textContent = targetInfo ? `(${targetInfo})` : '';
    document.body.style.overflow = 'hidden';
}

// 關閉共用輸出彈出視窗
function closeModal() {
    document.getElementById('outputModal').classList.remove('active');
    document.body.style.overflow = '';
    
    // 清除動態加入的確認按鈕，避免污染下次開啟
    const confirmBtn = document.getElementById('btn-confirm-restore');
    if (confirmBtn) confirmBtn.remove();
}

// 網頁載入時，檢查是否還有背景任務在跑 (維持掃描按鈕狀態)
document.addEventListener("DOMContentLoaded", checkScanButtonState);

// ==========================================
// 🛡️ 系統管理員雙重解鎖與 4 顆按鈕連動邏輯
// ==========================================
const ADMIN_BTN_IDS = ['btn-scan-visible', 'btn-clear-visible', 'btn-scan-global', 'btn-clear-global'];

document.addEventListener('DOMContentLoaded', () => {
    const appTitle = document.getElementById('app-title');

    function unlockAdminFeatures() {
        ADMIN_BTN_IDS.forEach(id => {
            const btn = document.getElementById(id);
            if (btn) btn.style.display = 'inline-block';
        });

        // 🌟 新增：解鎖樹狀圖裡的所有 🔍 預覽按鈕
        document.querySelectorAll('.debug-preview-btn').forEach(btn => {
            btn.style.display = 'inline-block';
        });
    }

    const urlParams = new URLSearchParams(window.location.search);
    if (urlParams.get('mode') === 'godmode') unlockAdminFeatures();

    let clickCount = 0;
    let clickTimer = null;
    if (appTitle) {
        appTitle.addEventListener('click', () => {
            clickCount++;
            if (clickCount === 5) {
                unlockAdminFeatures();
                alert('🔓 已解鎖系統維護者模式！'); 
                clickCount = 0; 
            }
            clearTimeout(clickTimer);
            clickTimer = setTimeout(() => { clickCount = 0; }, 1000); 
        });
    }
});

// 統一控制 4 顆按鈕的狀態
function setAdminButtonsState(isBusy, text = "⏳ 執行中...") {
    ADMIN_BTN_IDS.forEach(id => {
        const btn = document.getElementById(id);
        if (btn) {
            btn.disabled = isBusy;
            if (isBusy) {
                btn.classList.add('is-busy');
                if (id.includes('scan')) btn.innerText = text;
            } else {
                btn.classList.remove('is-busy');
                // 恢復原本的文字
                if (id === 'btn-scan-visible') btn.innerText = "掃描局部缺失";
                if (id === 'btn-scan-global') btn.innerText = "掃描全域缺失";
            }
        }
    });
}

function checkScanButtonState() {
    const btn = document.getElementById('btn-scan-visible');
    if (!btn) return;
    const scanLockUntil = localStorage.getItem('scanLockUntil');
    if (scanLockUntil && Date.now() < parseInt(scanLockUntil)) {
        lockScanButton(parseInt(scanLockUntil) - Date.now());
    }
}

function lockScanButton(durationMs) {
    setAdminButtonsState(true, "⏳ 背景掃描執行中...");
    setTimeout(() => {
        const currentMode = document.getElementById('configTask').value === 'form-full-config' ? 'full' : 'running';
        const treeContainer = document.getElementById(`tree-container-${currentMode}`);
        if (treeContainer && treeContainer.innerHTML.includes('leaf-container')) {
            enableGlobalScanButton();
        } else {
            setAdminButtonsState(true, "⏳ 請先載入樹狀圖");
        }
        localStorage.removeItem('scanLockUntil');
    }, durationMs);
}

function enableGlobalScanButton() {
    const scanLockUntil = localStorage.getItem('scanLockUntil');
    if (!scanLockUntil || Date.now() >= parseInt(scanLockUntil)) {
        setAdminButtonsState(false);
    }
}


// ============================================================================
// 🌟 7. 全域函數掛載 (Window Bindings - 供 HTML onClick 呼叫)
// ============================================================================

// --- 終端機綁定 ---
window.connectWebSocket = () => connectWebSocket(resetAllManagerData);
window.disconnectWebSocket = disconnectWebSocket;
window.injectCommand = injectCommand;
window.downloadTerminal = downloadTerminal;

// --- 頁籤與任務切換 ---
window.switchTab = switchTab;
window.switchQueryTask = switchQueryTask;
window.switchConfigTask = switchConfigTask;
window.startConfigTask = startConfigTask;
window.toggleQueryInputs = toggleQueryInputs;

// --- 查詢與 MAC Domain 精靈 ---
window.executeQuery = executeQuery;
window.fetchMacDomainConfig = fetchMacDomainConfig;
window.revertFormChanges = revertFormChanges;
window.toggleGroupVisibility = toggleGroupVisibility;
window.loadDsGroupData = loadDsGroupData;
window.loadUsGroupData = loadUsGroupData;
window.generateMacDomainCLI = generateMacDomainCLI;
window.executeBondingConfig = executeBondingConfig;
window.loadMacDomainList = loadMacDomainList;

// --- 系統設定與快取掃描 ---
window.openSystemSettings = openSystemSettings;
window.loadRealConfigForFilters = loadRealConfigForFilters;
window.saveSystemSettings = saveSystemSettings;
window.scanVisibleMissingOptions = scanVisibleMissingOptions;
window.scanGlobalMissingOptions = scanGlobalMissingOptions;
window.clearVisibleCache = clearVisibleCache;
window.clearGlobalCache = clearGlobalCache;
window.previewNodeCLI = previewNodeCLI;
window.switchFilterMode = switchFilterMode; 

// --- 樹狀圖展開與編輯操作 ---
window.expandAll = expandAll;
window.collapseAll = collapseAll;
window.startEditFolder = startEditFolder;
window.startEditLeaf = startEditLeaf;
window.cancelEditFolder = cancelEditFolder;
window.cancelEditLeaf = cancelEditLeaf;
window.previewFolderCLI = previewFolderCLI;
window.previewLeafCLI = previewLeafCLI;
window.hideSideCLI = hideSideCLI;
window.executeSideCLI = executeSideCLI;

// --- 其他 UI 輔助 ---
window.toggleChildCheckboxes = toggleChildCheckboxes;
window.updateParentCheckboxState = updateParentCheckboxState;
window.closeModal = closeModal;

// --- 備份與還原 ---
window.createSnapshot = createSnapshot;
window.loadBackupHistory = loadBackupHistory;
window.viewBackup = viewBackup;
window.deleteBackup = deleteBackup;
window.restoreBackup = restoreBackup; 
window.applyBackupFilters = applyBackupFilters; // 🌟 確保過濾器綁定到全域
