/* --- 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, apiGetLogLevels, apiSetLogLevel } 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 = `⏳ 背景掃描進度:${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); } // 切換「設備查詢」內的子任務表單 // ============================================================================ // 🌟 設備查詢動態清單快取 (新增) // ============================================================================ let cachedDeviceLists = { cms: null, rpds: null, host: null }; // 切換「設備查詢」內的子任務表單 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'); // 🌟 觸發背景動態載入 Datalist loadDynamicDatalist(selectedTaskId); } // 🌟 新增:背景載入 Datalist 邏輯 async function loadDynamicDatalist(taskType) { // 若尚未連線,則不觸發背景抓取 (不跳警告,默默 return) if (!isConnected) return; const host = document.getElementById('cmtsHost').value.trim(); const user = document.getElementById('cmtsUser').value.trim(); const pass = document.getElementById('cmtsPass').value.trim(); if (!host || !user || !pass) return; // 若切換了設備 IP,清空舊快取 if (cachedDeviceLists.host !== host) { cachedDeviceLists = { cms: null, rpds: null, host: host }; } if (taskType === 'form-cm-query' || taskType === 'form-cm-diagnostics') { const inputId = taskType === 'form-cm-query' ? 'queryTargetCm' : 'diagMacInput'; const listId = taskType === 'form-cm-query' ? 'cm-mac-list' : 'diag-cm-mac-list'; const defaultPlaceholder = taskType === 'form-cm-query' ? "例: 6467.7240.4076 (留白則查詢全體)" : "例如: 6467.7240.4076"; await fetchAndBindList('cms', inputId, listId, defaultPlaceholder, host, user, pass); } else if (taskType === 'form-rpd-query') { await fetchAndBindList('rpds', 'queryTargetRpd', 'rpd-vcvs-list', "例: 13:0 (留白則查詢全體)", host, user, pass); } } // 🌟 新增:執行 API 呼叫與 UX 狀態切換 async function fetchAndBindList(type, inputId, listId, defaultPlaceholder, host, user, pass) { const inputEl = document.getElementById(inputId); const datalistEl = document.getElementById(listId); if (!inputEl || !datalistEl) return; // 如果已經有快取,直接綁定並結束 if (cachedDeviceLists[type]) { populateDatalist(datalistEl, cachedDeviceLists[type]); return; } // 狀態 1:發出請求前 (鎖定輸入框,提示載入中) inputEl.disabled = true; inputEl.placeholder = "🔄 載入設備清單中..."; inputEl.style.backgroundColor = "#fdfefe"; try { let data; if (type === 'cms') { data = await apiListCms(host, user, pass); cachedDeviceLists.cms = data.cms || []; } else { data = await apiListRpds(host, user, pass); cachedDeviceLists.rpds = data.rpds || []; } // 狀態 2:請求成功 (塞入選項,恢復輸入框) populateDatalist(datalistEl, cachedDeviceLists[type]); inputEl.placeholder = defaultPlaceholder; } catch (error) { console.error(`[Background Task] Failed to load ${type}:`, error); // 狀態 3:請求失敗 (提示失敗,但仍解鎖讓使用者手動輸入) inputEl.placeholder = "⚠️ 清單載入失敗,請手動輸入"; } finally { inputEl.disabled = false; inputEl.style.backgroundColor = ""; // 恢復預設背景色 } } // 🌟 新增:將陣列轉換為