scm-harmonic-cmts-admin/static/app.js

643 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* --- 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
} from './api.js';
// 3. 共用工具模組 (Utils)
import { getGlobalConnectionInfo } from './utils.js';
// 4. 樹狀圖 UI 模組 (Tree UI)
import { buildTree, buildRealFilterTree, expandAll, collapseAll } from './tree-ui.js';
// 5. 編輯模式與鎖定模組 (Edit Mode)
import {
releaseAllLocks, startEditFolder, startEditLeaf,
cancelEditFolder, cancelEditLeaf, previewFolderCLI, previewLeafCLI,
hideSideCLI, executeSideCLI
} 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. 全域生命週期與閒置偵測 (Lifecycle & Idle Detection)
// ============================================================================
let idleTimer;
const IDLE_TIMEOUT = 300000; // 5 分鐘 (毫秒)
// 頁面載入與縮放初始化
window.onload = () => {
initTerminal();
resetIdleTimer(); // 頁面載入時啟動閒置計時器
};
window.onresize = () => { if (typeof fitAddon !== 'undefined' && fitAddon) fitAddon.fit(); };
// 閒置計時器重置邏輯 (超時將自動釋放所有編輯鎖定)
function resetIdleTimer() {
clearTimeout(idleTimer);
idleTimer = setTimeout(releaseAllLocks, IDLE_TIMEOUT);
}
// 監聽使用者的互動事件,只要有動作就重置計時器
window.addEventListener('mousemove', resetIdleTimer);
window.addEventListener('keydown', resetIdleTimer);
window.addEventListener('click', resetIdleTimer);
// ============================================================================
// 🌟 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');
}
// 切換「設備配置」內的子任務表單
function switchConfigTask() {
document.querySelectorAll('#config-tab .task-form').forEach(form => {
form.classList.remove('active');
form.style.display = 'none';
});
const selectedTaskId = document.getElementById('configTask').value;
const targetForm = document.getElementById(selectedTaskId);
if (targetForm) {
targetForm.classList.add('active');
targetForm.style.display = 'block';
}
}
// 啟動配置任務 (樹狀圖載入 或 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();
} else if (selectedTaskId === 'form-full-config') {
fetchFullConfig();
}
}
// 切換查詢輸入框的啟用狀態 (全域指令不需輸入目標)
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();
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 = '';
}
// ============================================================================
// 🌟 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');
outputEl.innerHTML = `<span style="color: #f39c12;">正在向 ${connInfo.host} 查詢中,請稍候...</span>`;
try {
const result = await apiExecuteQuery(queryType, target, connInfo.host, connInfo.user, connInfo.pass);
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) {
outputEl.style.color = "#e74c3c";
outputEl.textContent = `連線失敗: ${error}`;
}
}
// 載入完整設備配置並生成樹狀圖
async function fetchFullConfig() {
const loadingMsg = document.getElementById('loading-message');
const treeContainer = document.getElementById('tree-container');
const connInfo = getGlobalConnectionInfo();
if (!connInfo) return;
if (loadingMsg) loadingMsg.style.display = 'inline-block';
if (treeContainer) treeContainer.innerHTML = '';
try {
const result = await apiGetFullConfig(connInfo.host, connInfo.user, connInfo.pass);
if (result.status === 'success') {
if (treeContainer) {
treeContainer.innerHTML = buildTree(result.data);
enableGlobalScanButton();
}
} else {
if (treeContainer) treeContainer.innerHTML = `<div style="color: #c0392b; font-weight: bold; margin: 20px;">❌ 錯誤: ${result.message}</div>`;
}
} catch (error) {
if (treeContainer) treeContainer.innerHTML = `<div style="color: #c0392b; font-weight: bold; margin: 20px;">❌ 連線或解析失敗: ${error.message}</div>`;
} finally {
if (loadingMsg) loadingMsg.style.display = 'none';
}
}
// ============================================================================
// 🌟 4. 選項快取與背景掃描 (Cache & Background Scanning)
// ============================================================================
// 為現有的 input 加上下拉選項 (不破壞原有結構)
async function enhanceInputWithOptions(path, elementId) {
try {
const optRes = await fetch('/api/v1/cmts-leaf-options');
const optData = await optRes.json();
const cacheKey = path.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
const leafCache = optData[cacheKey];
const currentTime = Math.floor(Date.now() / 1000);
const CACHE_TTL = 86400 * 7;
if (leafCache && leafCache.options && leafCache.options.length > 0 && (currentTime - leafCache.updated_at < CACHE_TTL)) {
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 {
fetch('/api/v1/cmts-leaf-options/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ leaf_paths: [cacheKey] })
});
}
} catch (e) {
console.warn("選項外掛載入失敗,維持純文字輸入", e);
}
}
// 一鍵掃描缺失選項 (包含快取比對 + 防連點保護 + 僅掃描可見項目)
async function scanAllMissingOptions() {
const btn = document.getElementById('global-scan-btn');
const statusEl = document.getElementById('scan-status');
const treeContainer = document.getElementById('tree-container');
if (!treeContainer) return;
const LOCK_DURATION_MS = 60000;
localStorage.setItem('scanLockUntil', Date.now() + LOCK_DURATION_MS);
if (btn) {
btn.disabled = true;
btn.style.backgroundColor = "#bdc3c7";
btn.style.cursor = "not-allowed";
btn.innerText = "⏳ 掃描冷卻中...";
}
statusEl.innerHTML = `⏳ 正在比對快取資料...`;
statusEl.style.color = "#f39c12";
try {
const optRes = await fetch('/api/v1/cmts-leaf-options');
const optData = await optRes.json();
const currentTime = Math.floor(Date.now() / 1000);
const CACHE_TTL = 86400 * 7;
const allContainers = treeContainer.querySelectorAll('.leaf-container');
const pathsToSync = [];
allContainers.forEach(container => {
const isHiddenByFolder = container.closest('details:not([open])') !== null;
if (isHiddenByFolder) return;
const path = container.getAttribute('data-path');
if (path) {
const cacheKey = path.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
const leafCache = optData[cacheKey];
if (!leafCache || (currentTime - leafCache.updated_at >= CACHE_TTL)) {
if (!pathsToSync.includes(cacheKey)) pathsToSync.push(cacheKey);
}
}
});
if (pathsToSync.length === 0) {
statusEl.textContent = "✅ 當前展開的欄位都已有最新選項,無需掃描!";
statusEl.style.color = "#27ae60";
setTimeout(() => statusEl.textContent = "", 3000);
if (btn) {
btn.disabled = false;
btn.style.backgroundColor = "";
btn.style.cursor = "pointer";
btn.innerHTML = "🔍 掃描畫面缺失選項";
}
localStorage.removeItem('scanLockUntil');
return;
}
statusEl.innerHTML = `⏳ 正在背景掃描 <b>${pathsToSync.length}</b> 個欄位的選項...`;
statusEl.style.color = "#e67e22";
const response = await fetch('/api/v1/cmts-leaf-options/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ leaf_paths: pathsToSync })
});
if (response.ok) {
statusEl.innerHTML = `✅ 任務已送出!正在監控背景執行進度...`;
statusEl.style.color = "#3498db";
checkScanProgress(pathsToSync);
} else {
throw new Error("API 回應錯誤");
}
} catch (error) {
console.error("掃描請求失敗:", error);
statusEl.textContent = "❌ 掃描請求發送失敗,請檢查網路連線。";
statusEl.style.color = "#e74c3c";
}
}
// 智慧監控背景掃描進度
async function checkScanProgress(pendingPaths) {
const btn = document.getElementById('global-scan-btn');
const statusEl = document.getElementById('scan-status');
let attempts = 0;
const maxAttempts = 60; // 最多監控 5 分鐘 (每 5 秒查一次)
const interval = setInterval(async () => {
attempts++;
try {
const optRes = await fetch('/api/v1/cmts-leaf-options');
const optData = await optRes.json();
const remainingPaths = pendingPaths.filter(path => !optData[path]);
const doneCount = pendingPaths.length - remainingPaths.length;
if (remainingPaths.length === 0) {
clearInterval(interval);
localStorage.removeItem('scanLockUntil');
if (btn) {
btn.disabled = false;
btn.style.backgroundColor = "#8e44ad";
btn.style.cursor = "pointer";
btn.innerText = "🔍 一鍵掃描缺失選項";
}
statusEl.innerHTML = `✅ 掃描完美達成!快取已全面更新。`;
statusEl.style.color = "#27ae60";
setTimeout(() => statusEl.textContent = "", 5000);
} else if (attempts >= maxAttempts) {
clearInterval(interval);
localStorage.removeItem('scanLockUntil');
if (typeof enableGlobalScanButton === 'function') enableGlobalScanButton();
statusEl.innerHTML = `⚠️ 掃描耗時較長 (${doneCount}/${pendingPaths.length}),已轉為純背景執行。`;
statusEl.style.color = "#f39c12";
} else {
statusEl.innerHTML = `⏳ 背景掃描進度:<b>${doneCount} / ${pendingPaths.length}</b> 完成...`;
}
} catch (e) {
console.warn("監控進度時發生網路錯誤,稍後重試...", e);
}
}, 5000);
}
// 清除畫面選項快取功能 (精準可見度判定版)
async function clearVisibleCache() {
const elements = document.querySelectorAll('.leaf-container');
const pathsToClear = [];
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("畫面上沒有展開的選項可以清除!\n請先展開您想重抓的資料夾。");
if (!confirm(`確定要清除畫面上 ${pathsToClear.length} 個選項的快取嗎?\n(清除後請重新點擊「一鍵掃描」)`)) return;
try {
const result = await apiClearCache(pathsToClear);
alert(`✅ 成功清除 ${result.cleared_count} 筆快取!\n現在請點擊旁邊的「一鍵掃描缺失選項」來重新擷取。`);
} catch (error) {
console.error("清除快取失敗:", error);
alert("❌ 清除快取失敗,請檢查網路連線或後端 API 是否正確啟動。");
}
}
// ============================================================================
// 🌟 5. 系統設定與過濾器 (System Settings & Filters)
// ============================================================================
let currentHiddenKeys = [];
// 開啟系統設定並讀取目前的過濾器清單
async function openSystemSettings() {
try {
const result = await apiGetTreeFilters();
if (result.status === 'success') currentHiddenKeys = result.data;
} catch (error) {
console.error("讀取設定失敗:", error);
}
}
// 載入真實設備配置以供過濾器勾選
async function loadRealConfigForFilters() {
const connInfo = getGlobalConnectionInfo();
if (!connInfo) return alert("請先在畫面上方輸入設備連線資訊!");
const loadingMsg = document.getElementById('filter-loading-msg');
const container = document.getElementById('filter-checkboxes');
loadingMsg.style.display = 'inline-block';
container.style.display = 'none';
try {
const url = `/api/v1/cmts-full-config?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}&skip_filter=true`;
const response = await fetch(url);
const result = await response.json();
if (result.status === 'success') {
container.style.display = 'block';
container.innerHTML = buildRealFilterTree(result.data, "", currentHiddenKeys);
} else {
container.style.display = 'block';
container.innerHTML = `<div style="color: #c0392b; font-weight: bold;">❌ 錯誤: ${result.message}</div>`;
}
} catch (error) {
container.style.display = 'block';
container.innerHTML = `<div style="color: #c0392b; font-weight: bold;">❌ 連線失敗: ${error.message}</div>`;
} finally {
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 checkboxes = document.querySelectorAll('.filter-checkbox:checked');
const selectedHiddenKeys = Array.from(checkboxes).map(cb => cb.value);
try {
const result = await apiSaveTreeFilters(selectedHiddenKeys);
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("儲存設定時發生錯誤!");
}
}
// ============================================================================
// 🌟 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(event) {
if (event && event.target !== event.currentTarget && event.target.className !== 'modal-close') return;
document.getElementById('outputModal').classList.remove('active');
document.body.style.overflow = '';
}
// 網頁載入時,檢查是否還有背景任務在跑 (維持掃描按鈕狀態)
document.addEventListener("DOMContentLoaded", checkScanButtonState);
function checkScanButtonState() {
const btn = document.getElementById('global-scan-btn');
if (!btn) return;
const scanLockUntil = localStorage.getItem('scanLockUntil');
if (scanLockUntil && Date.now() < parseInt(scanLockUntil)) {
lockScanButton(parseInt(scanLockUntil) - Date.now());
}
}
function lockScanButton(durationMs) {
const btn = document.getElementById('global-scan-btn');
if (!btn) return;
btn.disabled = true;
btn.style.backgroundColor = "#e67e22";
btn.style.cursor = "not-allowed";
btn.innerText = "⏳ 背景掃描執行中...";
setTimeout(() => {
const treeContainer = document.getElementById('tree-container');
if (treeContainer && treeContainer.innerHTML.includes('leaf-container')) {
enableGlobalScanButton();
} else {
btn.disabled = true;
btn.style.backgroundColor = "#bdc3c7";
btn.innerText = "⏳ 請先載入樹狀圖";
}
localStorage.removeItem('scanLockUntil');
}, durationMs);
}
function enableGlobalScanButton() {
const btn = document.getElementById('global-scan-btn');
if (!btn) return;
const scanLockUntil = localStorage.getItem('scanLockUntil');
if (!scanLockUntil || Date.now() >= parseInt(scanLockUntil)) {
btn.disabled = false;
btn.style.backgroundColor = "#8e44ad";
btn.style.cursor = "pointer";
btn.innerText = "🔍 掃描畫面缺失選項";
}
}
// ============================================================================
// 🌟 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.clearVisibleCache = clearVisibleCache;
window.scanAllMissingOptions = scanAllMissingOptions;
// --- 樹狀圖展開與編輯操作 ---
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;