2026-04-29 10:00:40 +00:00
|
|
|
|
/* --- static/app.js --- */
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 📦 模組匯入區 (Imports)
|
|
|
|
|
|
// ============================================================================
|
2026-05-13 03:09:19 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 1. 終端機模組 (Terminal)
|
|
|
|
|
|
import { initTerminal, connectWebSocket, disconnectWebSocket, injectCommand, downloadTerminal, isConnected } from './terminal.js';
|
2026-05-08 08:50:12 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 2. API 網路請求模組 (API)
|
|
|
|
|
|
import {
|
|
|
|
|
|
apiGetFullConfig, apiClearCache, apiExecuteQuery,
|
2026-05-13 10:31:34 +00:00
|
|
|
|
apiGetTreeFilters, apiSaveTreeFilters,
|
2026-05-15 07:17:28 +00:00
|
|
|
|
apiSyncLeafOptionsStream, apiGetCmtsVersion,
|
|
|
|
|
|
apiGetScanStatus
|
2026-05-13 07:05:39 +00:00
|
|
|
|
} from './api.js';
|
2026-05-08 08:50:12 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 3. 共用工具模組 (Utils)
|
|
|
|
|
|
import { getGlobalConnectionInfo } from './utils.js';
|
2026-05-08 08:50:12 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 4. 樹狀圖 UI 模組 (Tree UI)
|
|
|
|
|
|
import { buildTree, buildRealFilterTree, expandAll, collapseAll } from './tree-ui.js';
|
2026-05-08 08:50:12 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 5. 編輯模式與鎖定模組 (Edit Mode)
|
|
|
|
|
|
import {
|
|
|
|
|
|
releaseAllLocks, startEditFolder, startEditLeaf,
|
|
|
|
|
|
cancelEditFolder, cancelEditLeaf, previewFolderCLI, previewLeafCLI,
|
2026-05-15 09:25:09 +00:00
|
|
|
|
hideSideCLI, executeSideCLI, previewNodeCLI
|
2026-05-13 07:05:39 +00:00
|
|
|
|
} from './edit-mode.js';
|
2026-05-08 08:50:12 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 6. MAC Domain 配置模組 (MAC Domain)
|
|
|
|
|
|
import {
|
|
|
|
|
|
hasMacDomainData, resetMacDomainData, loadMacDomainList, fetchMacDomainConfig,
|
|
|
|
|
|
populateFormFromData, revertFormChanges, toggleGroupVisibility,
|
|
|
|
|
|
loadDsGroupData, loadUsGroupData, generateMacDomainCLI, executeBondingConfig
|
|
|
|
|
|
} from './mac-domain.js';
|
2026-05-08 08:50:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// ============================================================================
|
2026-05-18 07:10:38 +00:00
|
|
|
|
// 🌟 1. 全域生命週期、閒置偵測與 SSE 廣播監聽
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// ============================================================================
|
2026-05-08 08:50:12 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
let idleTimer;
|
|
|
|
|
|
const IDLE_TIMEOUT = 300000; // 5 分鐘 (毫秒)
|
2026-05-18 07:10:38 +00:00
|
|
|
|
let globalSSE = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化全域 SSE 監聽器
|
|
|
|
|
|
function initGlobalSSE() {
|
|
|
|
|
|
if (globalSSE) return;
|
|
|
|
|
|
|
|
|
|
|
|
globalSSE = new EventSource('/api/v1/cmts-leaf-options/stream');
|
|
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-05-08 08:50:12 +00:00
|
|
|
|
|
2026-05-18 07:40:19 +00:00
|
|
|
|
// 🌟 關鍵修復:當使用者按 F5 重整或關閉網頁時,主動切斷 SSE 連線
|
|
|
|
|
|
window.addEventListener('beforeunload', () => {
|
|
|
|
|
|
if (globalSSE) {
|
|
|
|
|
|
globalSSE.close();
|
|
|
|
|
|
globalSSE = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 頁面載入與縮放初始化
|
|
|
|
|
|
window.onload = () => {
|
|
|
|
|
|
initTerminal();
|
|
|
|
|
|
resetIdleTimer(); // 頁面載入時啟動閒置計時器
|
2026-05-18 07:10:38 +00:00
|
|
|
|
initGlobalSSE(); // 啟動全域廣播監聽
|
2026-05-13 07:05:39 +00:00
|
|
|
|
};
|
|
|
|
|
|
window.onresize = () => { if (typeof fitAddon !== 'undefined' && fitAddon) fitAddon.fit(); };
|
2026-05-08 08:50:12 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 閒置計時器重置邏輯 (超時將自動釋放所有編輯鎖定)
|
|
|
|
|
|
function resetIdleTimer() {
|
|
|
|
|
|
clearTimeout(idleTimer);
|
|
|
|
|
|
idleTimer = setTimeout(releaseAllLocks, IDLE_TIMEOUT);
|
2026-05-08 08:50:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
window.addEventListener('mousemove', resetIdleTimer);
|
|
|
|
|
|
window.addEventListener('keydown', resetIdleTimer);
|
|
|
|
|
|
window.addEventListener('click', resetIdleTimer);
|
2026-04-30 10:52:55 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 🌟 2. 頁籤切換與 UI 狀態控制 (Tabs & UI State)
|
|
|
|
|
|
// ============================================================================
|
2026-04-30 10:52:55 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 切換主頁籤 (終端機 / 設備查詢 / 設備配置 / 系統設定)
|
2026-04-29 10:00:40 +00:00
|
|
|
|
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');
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
if(tabId === 'cli-tab' && typeof fitAddon !== 'undefined' && fitAddon) setTimeout(() => fitAddon.fit(), 50);
|
2026-04-29 10:00:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 切換「設備查詢」內的子任務表單
|
2026-04-29 10:00:40 +00:00
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 切換「設備配置」內的子任務表單
|
2026-04-29 10:00:40 +00:00
|
|
|
|
function switchConfigTask() {
|
2026-04-30 08:27:18 +00:00
|
|
|
|
document.querySelectorAll('#config-tab .task-form').forEach(form => {
|
|
|
|
|
|
form.classList.remove('active');
|
|
|
|
|
|
form.style.display = 'none';
|
|
|
|
|
|
});
|
2026-04-29 10:00:40 +00:00
|
|
|
|
const selectedTaskId = document.getElementById('configTask').value;
|
|
|
|
|
|
const targetForm = document.getElementById(selectedTaskId);
|
2026-04-30 08:27:18 +00:00
|
|
|
|
if (targetForm) {
|
|
|
|
|
|
targetForm.classList.add('active');
|
|
|
|
|
|
targetForm.style.display = 'block';
|
|
|
|
|
|
}
|
2026-04-29 10:00:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 啟動配置任務 (樹狀圖載入 或 MAC Domain 精靈)
|
2026-04-29 10:00:40 +00:00
|
|
|
|
function startConfigTask() {
|
2026-04-30 02:36:02 +00:00
|
|
|
|
if (!isConnected) {
|
2026-04-30 08:27:18 +00:00
|
|
|
|
alert("請先點擊畫面上方的「連線至 CMTS」成功後,再執行載入任務!");
|
2026-04-30 02:36:02 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-29 10:00:40 +00:00
|
|
|
|
switchConfigTask();
|
|
|
|
|
|
const selectedTaskId = document.getElementById('configTask').value;
|
|
|
|
|
|
|
|
|
|
|
|
if (selectedTaskId === 'form-bonding-config') {
|
2026-05-13 07:05:39 +00:00
|
|
|
|
if (hasMacDomainData() && !confirm("重新載入將會清除您目前未套用的設定,確定要繼續嗎?")) return;
|
2026-04-29 10:00:40 +00:00
|
|
|
|
resetAllManagerData();
|
|
|
|
|
|
loadMacDomainList();
|
2026-04-30 08:27:18 +00:00
|
|
|
|
} else if (selectedTaskId === 'form-full-config') {
|
|
|
|
|
|
fetchFullConfig();
|
2026-04-29 10:00:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 切換查詢輸入框的啟用狀態 (全域指令不需輸入目標)
|
2026-04-29 10:00:40 +00:00
|
|
|
|
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 (留白則查詢全體)";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 重置所有管理介面的資料與 UI 狀態
|
2026-04-29 10:00:40 +00:00
|
|
|
|
function resetAllManagerData() {
|
2026-05-13 07:05:39 +00:00
|
|
|
|
resetMacDomainData();
|
2026-04-29 10:00:40 +00:00
|
|
|
|
|
|
|
|
|
|
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 = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 🌟 3. 設備查詢與全域配置載入 (Query & Full Config)
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
// 執行綜合查詢 (CM / RPD)
|
2026-04-29 10:00:40 +00:00
|
|
|
|
async function executeQuery(mode) {
|
2026-05-13 07:05:39 +00:00
|
|
|
|
if (!isConnected) return alert("請先點擊畫面上方的「連線至 CMTS」按鈕!");
|
2026-04-29 10:00:40 +00:00
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-05-13 07:05:39 +00:00
|
|
|
|
const result = await apiExecuteQuery(queryType, target, connInfo.host, connInfo.user, connInfo.pass);
|
2026-04-29 10:00:40 +00:00
|
|
|
|
if (result.status === 'success') {
|
|
|
|
|
|
outputEl.style.color = "#e0e0e0";
|
2026-05-13 07:05:39 +00:00
|
|
|
|
outputEl.textContent = isDebug ? `[DEBUG] 實際發送指令: ${result.command}\n==================================================\n${result.data}` : result.data;
|
2026-04-29 10:00:40 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
outputEl.style.color = "#e74c3c";
|
|
|
|
|
|
outputEl.textContent = `查詢失敗:\n${result.detail || result.message}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
outputEl.style.color = "#e74c3c";
|
|
|
|
|
|
outputEl.textContent = `連線失敗: ${error}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 載入完整設備配置並生成樹狀圖
|
|
|
|
|
|
async function fetchFullConfig() {
|
|
|
|
|
|
const loadingMsg = document.getElementById('loading-message');
|
|
|
|
|
|
const treeContainer = document.getElementById('tree-container');
|
|
|
|
|
|
|
2026-04-29 10:00:40 +00:00
|
|
|
|
const connInfo = getGlobalConnectionInfo();
|
2026-05-13 07:05:39 +00:00
|
|
|
|
if (!connInfo) return;
|
2026-04-29 10:00:40 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
if (loadingMsg) loadingMsg.style.display = 'inline-block';
|
|
|
|
|
|
if (treeContainer) treeContainer.innerHTML = '';
|
2026-04-29 10:00:40 +00:00
|
|
|
|
|
2026-05-15 07:17:28 +00:00
|
|
|
|
let needAutoScan = false; // 🌟 標記是否需要自動掃描
|
|
|
|
|
|
|
2026-04-29 10:00:40 +00:00
|
|
|
|
try {
|
2026-05-15 07:17:28 +00:00
|
|
|
|
// --- 1. 版本守門員 ---
|
|
|
|
|
|
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');
|
|
|
|
|
|
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(allKeys);
|
|
|
|
|
|
alert("✅ 舊版快取已清空!系統載入配置後,將自動為您重新掃描選項。");
|
|
|
|
|
|
needAutoScan = true; // 🌟 觸發自動掃描旗標
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (vErr) {
|
|
|
|
|
|
console.warn("版本檢查失敗,略過", vErr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- 2. 載入完整配置 ---
|
2026-05-13 07:05:39 +00:00
|
|
|
|
const result = await apiGetFullConfig(connInfo.host, connInfo.user, connInfo.pass);
|
2026-04-29 10:00:40 +00:00
|
|
|
|
if (result.status === 'success') {
|
2026-05-13 07:05:39 +00:00
|
|
|
|
if (treeContainer) {
|
|
|
|
|
|
treeContainer.innerHTML = buildTree(result.data);
|
2026-05-15 07:17:28 +00:00
|
|
|
|
|
|
|
|
|
|
// 🌟 3. 檢查後端是否已經有人在掃描
|
|
|
|
|
|
const isSomeoneScanning = await apiGetScanStatus();
|
|
|
|
|
|
|
|
|
|
|
|
if (isSomeoneScanning) {
|
|
|
|
|
|
// B 使用者情境:有人在掃描了,反灰按鈕並提示
|
|
|
|
|
|
alert("💡 提示:系統正在背景更新設備選項快取,部分選單題示內容可能稍後才會顯示完整。");
|
|
|
|
|
|
lockScanButton(60000); // 使用您現有的鎖定 UI 函數
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// A 使用者情境:沒人在掃描,正常顯示按鈕
|
|
|
|
|
|
enableGlobalScanButton();
|
|
|
|
|
|
|
|
|
|
|
|
// 如果剛清空快取,自動啟動掃描 (改為全域掃描)
|
|
|
|
|
|
if (needAutoScan) {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
scanGlobalMissingOptions();
|
|
|
|
|
|
}, 500); // 給予 0.5 秒 UI 渲染緩衝
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-13 07:05:39 +00:00
|
|
|
|
}
|
2026-04-29 10:00:40 +00:00
|
|
|
|
} else {
|
2026-05-13 07:05:39 +00:00
|
|
|
|
if (treeContainer) treeContainer.innerHTML = `<div style="color: #c0392b; font-weight: bold; margin: 20px;">❌ 錯誤: ${result.message}</div>`;
|
2026-04-29 10:00:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2026-05-13 07:05:39 +00:00
|
|
|
|
if (treeContainer) treeContainer.innerHTML = `<div style="color: #c0392b; font-weight: bold; margin: 20px;">❌ 連線或解析失敗: ${error.message}</div>`;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (loadingMsg) loadingMsg.style.display = 'none';
|
2026-04-29 10:00:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 🌟 4. 選項快取與背景掃描 (Cache & Background Scanning)
|
|
|
|
|
|
// ============================================================================
|
2026-04-29 10:00:40 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 為現有的 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];
|
2026-04-29 10:00:40 +00:00
|
|
|
|
|
2026-05-15 07:17:28 +00:00
|
|
|
|
if (leafCache && leafCache.options && leafCache.options.length > 0) {
|
2026-05-13 07:05:39 +00:00
|
|
|
|
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);
|
2026-04-29 10:00:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 07:17:28 +00:00
|
|
|
|
// ============================================================================
|
2026-05-18 07:10:38 +00:00
|
|
|
|
// 🌟 核心功能:執行掃描 (廣播升級版)
|
2026-05-15 07:17:28 +00:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
async function performScan(isGlobal) {
|
|
|
|
|
|
const isSomeoneScanning = await apiGetScanStatus();
|
|
|
|
|
|
if (isSomeoneScanning) {
|
|
|
|
|
|
alert("目前已有其他使用者正在執行掃描,請稍候共享更新結果!");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-13 07:05:39 +00:00
|
|
|
|
|
2026-05-15 07:17:28 +00:00
|
|
|
|
const treeContainer = document.getElementById('tree-container');
|
2026-05-13 07:05:39 +00:00
|
|
|
|
if (!treeContainer) return;
|
2026-04-29 10:00:40 +00:00
|
|
|
|
|
2026-05-15 07:17:28 +00:00
|
|
|
|
const statusEl = document.getElementById('scan-status');
|
2026-05-13 07:05:39 +00:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const optRes = await fetch('/api/v1/cmts-leaf-options');
|
|
|
|
|
|
const optData = await optRes.json();
|
2026-04-29 10:00:40 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
const allContainers = treeContainer.querySelectorAll('.leaf-container');
|
|
|
|
|
|
const pathsToSync = [];
|
2026-04-29 10:00:40 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
allContainers.forEach(container => {
|
|
|
|
|
|
const isHiddenByFolder = container.closest('details:not([open])') !== null;
|
2026-05-15 07:17:28 +00:00
|
|
|
|
if (isGlobal || !isHiddenByFolder) {
|
|
|
|
|
|
const path = container.getAttribute('data-path');
|
|
|
|
|
|
if (path) {
|
|
|
|
|
|
const cacheKey = path.replace(/::/g, ' ').replace(/\s*\[\d+\]/g, '');
|
|
|
|
|
|
const leafCache = optData[cacheKey];
|
|
|
|
|
|
if (!leafCache) {
|
|
|
|
|
|
if (!pathsToSync.includes(cacheKey)) pathsToSync.push(cacheKey);
|
|
|
|
|
|
}
|
2026-05-13 07:05:39 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (pathsToSync.length === 0) {
|
2026-05-15 07:17:28 +00:00
|
|
|
|
statusEl.textContent = isGlobal ? "✅ 全域所有欄位都已有最新選項,無需掃描!" : "✅ 局部展開的欄位都已有最新選項,無需掃描!";
|
2026-05-13 07:05:39 +00:00
|
|
|
|
statusEl.style.color = "#27ae60";
|
|
|
|
|
|
setTimeout(() => statusEl.textContent = "", 3000);
|
|
|
|
|
|
return;
|
2026-04-29 10:00:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 07:10:38 +00:00
|
|
|
|
// 直接發送 POST 請求觸發背景任務,UI 變化交給 SSE 處理
|
|
|
|
|
|
const response = await fetch('/api/v1/cmts-leaf-options/sync', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({ leaf_paths: pathsToSync })
|
2026-05-13 10:31:34 +00:00
|
|
|
|
});
|
2026-05-18 07:10:38 +00:00
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
if (result.status === 'busy') {
|
|
|
|
|
|
alert(result.message);
|
|
|
|
|
|
}
|
2026-05-13 10:31:34 +00:00
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
2026-05-18 07:10:38 +00:00
|
|
|
|
console.error("掃描請求發送失敗:", error);
|
2026-05-13 10:31:34 +00:00
|
|
|
|
statusEl.textContent = "❌ 掃描請求發送失敗,請檢查網路連線。";
|
|
|
|
|
|
statusEl.style.color = "#e74c3c";
|
|
|
|
|
|
}
|
2026-04-30 08:27:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 07:17:28 +00:00
|
|
|
|
// ============================================================================
|
2026-05-18 07:10:38 +00:00
|
|
|
|
// 🌟 核心功能:清除快取 (廣播升級版)
|
2026-05-15 07:17:28 +00:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
async function performClear(isGlobal) {
|
|
|
|
|
|
const isSomeoneScanning = await apiGetScanStatus();
|
|
|
|
|
|
if (isSomeoneScanning) {
|
|
|
|
|
|
alert("⚠️ 系統正在進行全域選項掃描更新中!\n為避免資料衝突,請稍候掃描完成再執行清除動作。");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let pathsToClear = [];
|
2026-04-30 08:27:18 +00:00
|
|
|
|
|
2026-05-15 07:17:28 +00:00
|
|
|
|
if (isGlobal) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const optRes = await fetch('/api/v1/cmts-leaf-options');
|
|
|
|
|
|
const optData = await optRes.json();
|
|
|
|
|
|
pathsToClear = Object.keys(optData).filter(k => k !== '__metadata__');
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error(e);
|
|
|
|
|
|
return alert("❌ 無法取得全域快取清單。");
|
2026-05-13 07:05:39 +00:00
|
|
|
|
}
|
2026-05-15 07:17:28 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
const elements = document.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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-04-30 08:27:18 +00:00
|
|
|
|
|
2026-05-15 07:17:28 +00:00
|
|
|
|
if (pathsToClear.length === 0) return alert(isGlobal ? "目前沒有任何快取可以清除!" : "局部沒有展開的選項可以清除!\n請先展開您想重抓的資料夾。");
|
|
|
|
|
|
|
|
|
|
|
|
const msg = isGlobal
|
|
|
|
|
|
? `確定要清除「全域」共 ${pathsToClear.length} 個選項的快取嗎?\n(這將會清空所有已儲存的下拉選單資料)`
|
2026-05-18 07:10:38 +00:00
|
|
|
|
: `確定要清除局部 ${pathsToClear.length} 個選項的快取嗎?\n(清除後將自動為您重新掃描)`;
|
2026-05-15 07:17:28 +00:00
|
|
|
|
|
|
|
|
|
|
if (!confirm(msg)) return;
|
2026-04-30 08:27:18 +00:00
|
|
|
|
|
|
|
|
|
|
try {
|
2026-05-13 07:05:39 +00:00
|
|
|
|
const result = await apiClearCache(pathsToClear);
|
2026-05-18 07:10:38 +00:00
|
|
|
|
const statusEl = document.getElementById('scan-status');
|
|
|
|
|
|
if (statusEl) {
|
|
|
|
|
|
statusEl.textContent = `✅ 成功清除 ${result.cleared_count} 筆快取!準備重新掃描...`;
|
|
|
|
|
|
statusEl.style.color = "#27ae60";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清除完畢後,自動呼叫簡化版的 performScan
|
|
|
|
|
|
await performScan(isGlobal);
|
|
|
|
|
|
|
2026-04-30 08:27:18 +00:00
|
|
|
|
} catch (error) {
|
2026-05-13 07:05:39 +00:00
|
|
|
|
console.error("清除快取失敗:", error);
|
|
|
|
|
|
alert("❌ 清除快取失敗,請檢查網路連線或後端 API 是否正確啟動。");
|
2026-04-30 08:27:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 07:17:28 +00:00
|
|
|
|
// 綁定給 HTML 呼叫的 4 個獨立函數
|
|
|
|
|
|
function scanVisibleMissingOptions() { performScan(false); }
|
|
|
|
|
|
function scanGlobalMissingOptions() { performScan(true); }
|
|
|
|
|
|
function clearVisibleCache() { performClear(false); }
|
|
|
|
|
|
function clearGlobalCache() { performClear(true); }
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 🌟 5. 系統設定與過濾器 (System Settings & Filters)
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
2026-04-30 08:27:18 +00:00
|
|
|
|
let currentHiddenKeys = [];
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 開啟系統設定並讀取目前的過濾器清單
|
2026-04-30 08:27:18 +00:00
|
|
|
|
async function openSystemSettings() {
|
|
|
|
|
|
try {
|
2026-05-13 07:05:39 +00:00
|
|
|
|
const result = await apiGetTreeFilters();
|
|
|
|
|
|
if (result.status === 'success') currentHiddenKeys = result.data;
|
2026-04-30 08:27:18 +00:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("讀取設定失敗:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 載入真實設備配置以供過濾器勾選
|
2026-04-30 08:27:18 +00:00
|
|
|
|
async function loadRealConfigForFilters() {
|
|
|
|
|
|
const connInfo = getGlobalConnectionInfo();
|
2026-05-13 07:05:39 +00:00
|
|
|
|
if (!connInfo) return alert("請先在畫面上方輸入設備連線資訊!");
|
2026-04-30 08:27:18 +00:00
|
|
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 當點擊「父節點」時:同步勾選/取消所有子節點
|
2026-05-04 10:26:12 +00:00
|
|
|
|
function toggleChildCheckboxes(parentCheckbox) {
|
|
|
|
|
|
const details = parentCheckbox.closest('details');
|
|
|
|
|
|
if (details) {
|
|
|
|
|
|
const childCheckboxes = details.querySelectorAll('.filter-children-container .filter-checkbox');
|
2026-05-13 07:05:39 +00:00
|
|
|
|
childCheckboxes.forEach(cb => cb.checked = parentCheckbox.checked);
|
2026-05-04 10:26:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
updateParentCheckboxState(parentCheckbox);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 當點擊「子節點」時:往上檢查父節點是否該被勾選
|
2026-05-04 10:26:12 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 儲存系統設定 (過濾器)
|
2026-04-30 08:27:18 +00:00
|
|
|
|
async function saveSystemSettings() {
|
|
|
|
|
|
const checkboxes = document.querySelectorAll('.filter-checkbox:checked');
|
|
|
|
|
|
const selectedHiddenKeys = Array.from(checkboxes).map(cb => cb.value);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2026-05-13 07:05:39 +00:00
|
|
|
|
const result = await apiSaveTreeFilters(selectedHiddenKeys);
|
2026-04-30 08:27:18 +00:00
|
|
|
|
if (result.status === 'success') {
|
|
|
|
|
|
const statusText = document.getElementById('settings-status');
|
|
|
|
|
|
statusText.style.display = 'inline-block';
|
2026-05-13 07:05:39 +00:00
|
|
|
|
currentHiddenKeys = selectedHiddenKeys;
|
2026-04-30 08:27:18 +00:00
|
|
|
|
setTimeout(() => { statusText.style.display = 'none'; }, 3000);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("儲存設定失敗:", error);
|
|
|
|
|
|
alert("儲存設定時發生錯誤!");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 🌟 6. 共用 UI 元件與狀態管理 (Modals & Button States)
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
// 開啟共用輸出彈出視窗
|
2026-04-29 10:00:40 +00:00
|
|
|
|
function openModal(targetInfo) {
|
|
|
|
|
|
document.getElementById('outputModal').classList.add('active');
|
|
|
|
|
|
document.getElementById('modalTargetInfo').textContent = targetInfo ? `(${targetInfo})` : '';
|
|
|
|
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 關閉共用輸出彈出視窗
|
2026-04-29 10:00:40 +00:00
|
|
|
|
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 = '';
|
2026-05-04 10:26:12 +00:00
|
|
|
|
}
|
2026-05-08 08:50:12 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// 網頁載入時,檢查是否還有背景任務在跑 (維持掃描按鈕狀態)
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", checkScanButtonState);
|
2026-05-08 08:50:12 +00:00
|
|
|
|
|
2026-05-14 07:42:18 +00:00
|
|
|
|
// ==========================================
|
2026-05-15 07:17:28 +00:00
|
|
|
|
// 🛡️ 系統管理員雙重解鎖與 4 顆按鈕連動邏輯
|
2026-05-14 07:42:18 +00:00
|
|
|
|
// ==========================================
|
2026-05-15 07:17:28 +00:00
|
|
|
|
const ADMIN_BTN_IDS = ['btn-scan-visible', 'btn-clear-visible', 'btn-scan-global', 'btn-clear-global'];
|
|
|
|
|
|
|
2026-05-14 07:42:18 +00:00
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
|
const appTitle = document.getElementById('app-title');
|
|
|
|
|
|
|
|
|
|
|
|
function unlockAdminFeatures() {
|
2026-05-15 07:17:28 +00:00
|
|
|
|
ADMIN_BTN_IDS.forEach(id => {
|
|
|
|
|
|
const btn = document.getElementById(id);
|
|
|
|
|
|
if (btn) btn.style.display = 'inline-block';
|
|
|
|
|
|
});
|
2026-05-15 09:25:09 +00:00
|
|
|
|
|
|
|
|
|
|
// 🌟 新增:解鎖樹狀圖裡的所有 🔍 預覽按鈕
|
|
|
|
|
|
document.querySelectorAll('.debug-preview-btn').forEach(btn => {
|
|
|
|
|
|
btn.style.display = 'inline-block';
|
|
|
|
|
|
});
|
2026-05-14 07:42:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
2026-05-15 07:17:28 +00:00
|
|
|
|
if (urlParams.get('mode') === 'godmode') unlockAdminFeatures();
|
2026-05-14 07:42:18 +00:00
|
|
|
|
|
|
|
|
|
|
let clickCount = 0;
|
|
|
|
|
|
let clickTimer = null;
|
|
|
|
|
|
if (appTitle) {
|
|
|
|
|
|
appTitle.addEventListener('click', () => {
|
|
|
|
|
|
clickCount++;
|
|
|
|
|
|
if (clickCount === 5) {
|
|
|
|
|
|
unlockAdminFeatures();
|
|
|
|
|
|
alert('🔓 已解鎖系統維護者模式!');
|
|
|
|
|
|
clickCount = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
clearTimeout(clickTimer);
|
2026-05-15 07:17:28 +00:00
|
|
|
|
clickTimer = setTimeout(() => { clickCount = 0; }, 1000);
|
2026-05-14 07:42:18 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-15 07:17:28 +00:00
|
|
|
|
// 統一控制 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 = "🌍 掃描全域缺失選項";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 08:50:12 +00:00
|
|
|
|
function checkScanButtonState() {
|
2026-05-15 07:17:28 +00:00
|
|
|
|
const btn = document.getElementById('btn-scan-visible');
|
2026-05-08 08:50:12 +00:00
|
|
|
|
if (!btn) return;
|
|
|
|
|
|
const scanLockUntil = localStorage.getItem('scanLockUntil');
|
2026-05-13 07:05:39 +00:00
|
|
|
|
if (scanLockUntil && Date.now() < parseInt(scanLockUntil)) {
|
|
|
|
|
|
lockScanButton(parseInt(scanLockUntil) - Date.now());
|
2026-05-08 08:50:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function lockScanButton(durationMs) {
|
2026-05-15 07:17:28 +00:00
|
|
|
|
setAdminButtonsState(true, "⏳ 背景掃描執行中...");
|
2026-05-08 08:50:12 +00:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
const treeContainer = document.getElementById('tree-container');
|
|
|
|
|
|
if (treeContainer && treeContainer.innerHTML.includes('leaf-container')) {
|
|
|
|
|
|
enableGlobalScanButton();
|
|
|
|
|
|
} else {
|
2026-05-15 07:17:28 +00:00
|
|
|
|
setAdminButtonsState(true, "⏳ 請先載入樹狀圖");
|
2026-05-08 08:50:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
localStorage.removeItem('scanLockUntil');
|
|
|
|
|
|
}, durationMs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function enableGlobalScanButton() {
|
|
|
|
|
|
const scanLockUntil = localStorage.getItem('scanLockUntil');
|
|
|
|
|
|
if (!scanLockUntil || Date.now() >= parseInt(scanLockUntil)) {
|
2026-05-15 07:17:28 +00:00
|
|
|
|
setAdminButtonsState(false);
|
2026-05-08 08:50:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 🌟 7. 全域函數掛載 (Window Bindings - 供 HTML onClick 呼叫)
|
|
|
|
|
|
// ============================================================================
|
2026-05-08 08:50:12 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// --- 終端機綁定 ---
|
|
|
|
|
|
window.connectWebSocket = () => connectWebSocket(resetAllManagerData);
|
|
|
|
|
|
window.disconnectWebSocket = disconnectWebSocket;
|
|
|
|
|
|
window.injectCommand = injectCommand;
|
|
|
|
|
|
window.downloadTerminal = downloadTerminal;
|
2026-05-08 08:50:12 +00:00
|
|
|
|
|
2026-05-13 07:05:39 +00:00
|
|
|
|
// --- 頁籤與任務切換 ---
|
|
|
|
|
|
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;
|
2026-05-15 07:17:28 +00:00
|
|
|
|
window.scanVisibleMissingOptions = scanVisibleMissingOptions;
|
|
|
|
|
|
window.scanGlobalMissingOptions = scanGlobalMissingOptions;
|
2026-05-13 07:05:39 +00:00
|
|
|
|
window.clearVisibleCache = clearVisibleCache;
|
2026-05-15 07:17:28 +00:00
|
|
|
|
window.clearGlobalCache = clearGlobalCache;
|
2026-05-15 09:25:09 +00:00
|
|
|
|
window.previewNodeCLI = previewNodeCLI;
|
2026-05-13 07:05:39 +00:00
|
|
|
|
|
|
|
|
|
|
// --- 樹狀圖展開與編輯操作 ---
|
|
|
|
|
|
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;
|