feat: 1.重構快取掃描邏輯,新增局部與全域操作分離, 2.GUI按鈕格規格統一化。

This commit is contained in:
swpa 2026-05-15 15:17:28 +08:00
parent 682c9341a7
commit 03814b29dc
11 changed files with 150053 additions and 34403 deletions

File diff suppressed because it is too large Load Diff

111987
cmts_leaf_options_cache.json.bk Normal file

File diff suppressed because it is too large Load Diff

View File

@ -150,6 +150,8 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list):
BATCH_SIZE = 30
batches = [leaf_paths[i:i + BATCH_SIZE] for i in range(0, len(leaf_paths), BATCH_SIZE)]
cmts_version = "unknown" # 🌟 新增:用來記錄當前爬蟲抓到的版本
for batch_idx, batch in enumerate(batches):
print(f"🔄 正在處理第 {batch_idx + 1}/{len(batches)} 批次 (共 {len(batch)} 個路徑)...")
conn = None
@ -172,6 +174,15 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list):
break
return output
# 🌟 新增:在第一批次連線時,先抓取設備版本
if cmts_version == "unknown":
process.stdin.write("show version\n")
await process.stdin.drain()
version_output = await read_until_quiet(timeout=1.5)
match = re.search(r"(?:infra|vcmts-cd-0)\s+([\w\.\-]+)", version_output)
if match:
cmts_version = match.group(1)
process.stdin.write("config\n")
await process.stdin.drain()
await read_until_quiet(timeout=1.5)
@ -198,14 +209,10 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list):
}
# --- 步驟 2判斷是否需要發送 Enter ---
# 🌟 配合新規則:如果 '?' 已經明確告訴我們這是 <格式> 欄位,直接跳過 Enter
if q_data.get("is_format_only"):
parsed_data["type"] = "string_or_number"
# if q_data.get("format_desc"):
# parsed_data["hint"] += f"\nFormat: {q_data['format_desc']}"
elif not q_data["options"]:
# 原本的 Enter 邏輯
process.stdin.write(f"{path}\n")
await process.stdin.drain()
@ -216,9 +223,6 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list):
parsed_data["options"] = enter_data["options"]
parsed_data["current_value"] = enter_data["current_value"]
# if "format_desc" in enter_data:
# parsed_data["hint"] += f"\nFormat: {enter_data['format_desc']}"
if enter_data["type"] != "unknown":
process.stdin.write("\x03")
await process.stdin.drain()
@ -231,7 +235,6 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list):
result_data[path] = parsed_data
# 🌟 核心修改:每處理完一個路徑,就推播一次進度!
processed_count += 1
event_data = {
"event": "progress",
@ -251,6 +254,7 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list):
finally:
if conn: conn.close()
# --- 步驟 3寫入快取檔 ---
try:
cache_file = "cmts_leaf_options_cache.json"
cache_data = {}
@ -258,6 +262,15 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list):
with open(cache_file, "r", encoding="utf-8") as f:
cache_data = json.load(f)
# 🌟 新增:寫入 Metadata (版本與掃描時間)
if "__metadata__" not in cache_data:
cache_data["__metadata__"] = {}
if cmts_version != "unknown":
cache_data["__metadata__"]["cmts_version"] = cmts_version
cache_data["__metadata__"]["last_scanned"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
current_ts = int(time.time())
for p in batch:
if p in result_data:
@ -266,7 +279,7 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list):
with open(cache_file, "w", encoding="utf-8") as f:
json.dump(cache_data, f, indent=4, ensure_ascii=False)
print(f"💾 [Debug] 第 {batch_idx + 1}/{len(batches)} 批次已提早寫入快取檔!")
print(f"💾 [Debug] 第 {batch_idx + 1}/{len(batches)} 批次已提早寫入快取檔!(版本: {cmts_version})")
except Exception as e:
print(f"⚠️ 提早寫入快取失敗: {e}")

View File

@ -23,8 +23,8 @@
<input type="password" id="cmtsPass" placeholder="密碼" value="nsgadmin" style="width: 100px;">
<div style="margin-left: 15px; display: flex; align-items: center; gap: 10px;">
<button onclick="connectWebSocket()" id="btnConnect" style="background-color: #27ae60;">連線至 CMTS</button>
<button onclick="disconnectWebSocket()" id="btnDisconnect" style="background-color: #c0392b; display: none;">斷開連線</button>
<button onclick="connectWebSocket()" id="btnConnect" class="btn-modern btn-connect">🟢 連線至 CMTS</button>
<button onclick="disconnectWebSocket()" id="btnDisconnect" class="btn-modern btn-disconnect" style="display: none;">斷開連線</button>
<span id="wsStatus" style="color: #7f8c8d; font-size: 14px; font-weight: bold;">狀態:未連線</span>
</div>
</div>
@ -46,10 +46,10 @@
<option value="show cable rpd">RPD 狀態 (show cable rpd)</option>
<option value="show running-config | nomore">系統實時設定檔 (show running-config | nomore)</option>
</select>
<button onclick="injectCommand()" style="background-color: #8e44ad;">送出指令</button>
<button onclick="injectCommand()" class="btn-modern btn-load">送出指令</button>
<div style="margin-left: auto; display: flex; gap: 8px;">
<button onclick="downloadTerminal()" style="background-color: #7f8c8d;">💾 下載紀錄</button>
<button onclick="downloadTerminal()" class="btn-modern btn-slate">💾 下載紀錄</button>
</div>
</div>
@ -109,7 +109,7 @@
</div>
</div>
<div class="form-actions">
<button onclick="executeQuery('Cm')">執行查詢</button>
<button onclick="executeQuery('Cm')" class="btn-modern btn-load">執行查詢</button>
<div style="display: flex; align-items: center;">
<input type="checkbox" id="debugQueryCm" style="width: 18px; height: 18px; margin: 0; cursor: pointer;">
<label for="debugQueryCm" style="cursor: pointer; color: #7f8c8d; margin-left: 8px; font-size: 14px;">🐞 顯示底層指令 (Debug)</label>
@ -144,7 +144,7 @@
</div>
</div>
<div class="form-actions">
<button onclick="executeQuery('Rpd')" style="background-color: #e67e22;">執行查詢</button>
<button onclick="executeQuery('Rpd')" class="btn-modern btn-load">執行查詢</button>
<div style="display: flex; align-items: center;">
<input type="checkbox" id="debugQueryRpd" style="width: 18px; height: 18px; margin: 0; cursor: pointer;">
<label for="debugQueryRpd" style="cursor: pointer; color: #7f8c8d; margin-left: 8px; font-size: 14px;">🐞 顯示底層指令 (Debug)</label>
@ -164,7 +164,7 @@
<option value="form-bonding-config">⚠️ MAC Domain 狀態感知配置精靈</option>
<option value="form-full-config">🌳 完整設備配置樹狀圖</option>
</select>
<button id="btn-load-task" onclick="startConfigTask()" style="background-color: #95a5a6; color: white; margin-left: 10px; cursor: not-allowed;" disabled>🚀 載入任務</button>
<button id="btn-load-task" onclick="startConfigTask()" class="btn-modern btn-load" style="margin-left: 10px;" disabled>🚀 載入任務</button>
</div>
<!-- 表單 3MAC Domain 配置精靈 -->
@ -176,7 +176,7 @@
<select id="cfgMacDomain" style="width: 150px; padding: 6px; font-weight: bold; border: 1px solid #fadbd8; border-radius: 4px; background-color: #fff;">
<option value="">請先點擊上方載入任務...</option>
</select>
<button onclick="fetchMacDomainConfig()" style="background-color: #d35400;">🔍 讀取現有配置</button>
<button onclick="fetchMacDomainConfig()" class="btn-modern btn-load">🔍 讀取現有配置</button>
<span id="fetchStatus" style="margin-left: 10px; font-size: 14px; color: #7f8c8d;">請先讀取設備狀態...</span>
</div>
@ -240,11 +240,9 @@
<div id="cliPreviewContainer" style="display: none; background: #2c3e50; color: #ecf0f1; padding: 15px; border-radius: 6px; margin-top: 30px; margin-bottom: 20px; font-family: monospace; white-space: pre-wrap;"></div>
<div class="form-actions">
<button id="btnRevert" onclick="revertFormChanges()" style="background-color: #7f8c8d; color: white; margin-right: 15px;">
🔄 復原重填
</button>
<button onclick="generateMacDomainCLI()" style="background-color: #f39c12; color: white;">⚙️ 產生絕對路徑指令預覽</button>
<button id="btnDeployConfig" onclick="executeBondingConfig()" style="background-color: #c0392b; color: white; display: none;" disabled>🚀 正式套用設定</button>
<button id="btnRevert" onclick="revertFormChanges()" class="btn-modern btn-disconnect" style="margin-right: 15px;">🔄 復原重填</button>
<button onclick="generateMacDomainCLI()" class="btn-modern btn-scan">⚙️ 產生絕對路徑指令預覽</button>
<button id="btnDeployConfig" onclick="executeBondingConfig()" class="btn-modern btn-save" style="display: none;" disabled>🚀 正式套用設定</button>
</div>
</div>
</div>
@ -254,16 +252,19 @@
<h3 style="display: flex; align-items: center; margin-top: 0; margin-bottom: 10px; font-size: 16px; color: #2c3e50;">
🌳 完整設備配置樹狀圖
<!-- 🌟 掃描按鈕 -->
<button id="global-scan-btn" onclick="scanAllMissingOptions()" disabled
style="display: none; padding: 6px 12px; background-color: #8e44ad; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold; margin-left: 20px; transition: all 0.3s; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
🔍 掃描畫面缺失選項
<!-- 🌟 掃描與清除按鈕群組 -->
<button id="btn-scan-visible" onclick="scanVisibleMissingOptions()" class="btn-modern btn-scan" disabled style="display: none; margin-left: 20px;">
🔍 掃描局部缺失選項
</button>
<button id="btn-clear-visible" onclick="clearVisibleCache()" class="btn-modern btn-clear" style="display: none; margin-left: 10px;">
🗑️ 清除局部選項快取
</button>
<!-- 🌟 請在這裡加入這段全新的按鈕 -->
<button id="clearCacheBtn" onclick="clearVisibleCache()"
style="display: none; padding: 6px 12px; background-color: #f39c12; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: bold; margin-left: 10px; transition: all 0.3s; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
🔄 清除畫面選項快取
<button id="btn-scan-global" onclick="scanGlobalMissingOptions()" class="btn-modern btn-scan" disabled style="display: none; margin-left: 10px;">
🌍 掃描全域缺失選項
</button>
<button id="btn-clear-global" onclick="clearGlobalCache()" class="btn-modern btn-clear" style="display: none; margin-left: 10px;">
🔥 清除全域快取
</button>
<!-- 🌟 新增:掃描進度提示 -->
@ -301,11 +302,11 @@
<div style="display: flex; align-items: center; gap: 10px;">
<!-- 預覽模式按鈕 -->
<button id="btn-side-cancel" onclick="hideSideCLI()" style="background-color: #7f8c8d; padding: 5px 12px; font-size: 13px;">取消</button>
<button id="btn-side-confirm" onclick="executeSideCLI()" style="background-color: #27ae60; padding: 5px 12px; font-size: 13px;">🚀 確認寫入</button>
<button id="btn-side-cancel" onclick="hideSideCLI()" class="btn-modern btn-disconnect" style="padding: 5px 12px; font-size: 13px;">取消</button>
<button id="btn-side-confirm" onclick="executeSideCLI()" class="btn-modern btn-save" style="padding: 5px 12px; font-size: 13px;">🚀 確認寫入</button>
<!-- 執行結果模式按鈕 (預設隱藏) -->
<button id="btn-side-close" onclick="hideSideCLI()" style="background-color: #3498db; padding: 5px 12px; font-size: 13px; display: none;">✅ 完成並關閉</button>
<button id="btn-side-close" onclick="hideSideCLI()" class="btn-modern btn-load" style="padding: 5px 12px; font-size: 13px; display: none;">✅ 完成並關閉</button>
<div style="width: 1px; height: 20px; background-color: #7f8c8d; margin: 0 5px;"></div>
<span onclick="hideSideCLI()" style="color: #bdc3c7; font-size: 26px; cursor: pointer; line-height: 1;" title="關閉面板">&times;</span>

View File

@ -16,6 +16,14 @@ CACHE_FILE = "cmts_leaf_options_cache.json"
# 🌟 1. 改用全域布林變數,狀態切換更即時,消滅非同步時間差
IS_SCANNING = False
# ==========================================
# 🌟 新增:讓前端查詢目前是否有人正在掃描
# ==========================================
@router.get("/scan-status")
async def get_scan_status():
global IS_SCANNING
return {"is_scanning": IS_SCANNING}
class SyncOptionsRequest(BaseModel):
leaf_paths: List[str]

View File

@ -32,7 +32,7 @@ def parse_cm_output(raw_text: str) -> list:
async def get_cable_modems():
try:
net_connect = ConnectHandler(**CMTS_DEVICE)
raw_output = net_connect.send_command("show cable modem")
raw_output = str(net_connect.send_command("show cable modem"))
net_connect.disconnect()
structured_data = parse_cm_output(raw_output)
return {"status": "success", "total_count": len(structured_data), "data": structured_data}
@ -121,7 +121,7 @@ async def get_mac_domain_config(target: str, host: str, username: str, password:
cli_command = f"show running-config cable mac-domain {target.strip()} | nomore"
net_connect = ConnectHandler(**device)
raw_output = net_connect.send_command(cli_command, read_timeout=15)
raw_output = str(net_connect.send_command(cli_command, read_timeout=15))
net_connect.disconnect()
# 💡 正名工程:更新字典的 Key 為一致性的命名
@ -190,7 +190,7 @@ async def get_cmts_mac_domain_list(
device.update({'host': host, 'username': username, 'password': password})
net_connect = ConnectHandler(**device)
raw_output = net_connect.send_command_timing("show running-config cable mac-domain ?")
raw_output = str(net_connect.send_command_timing("show running-config cable mac-domain ?"))
net_connect.disconnect()
import re
@ -200,3 +200,29 @@ async def get_cmts_mac_domain_list(
return {"status": "success", "data": mac_domains}
except Exception as e:
return {"status": "error", "message": str(e)}
@router.get("/cmts-version")
async def get_cmts_version(
host: str = Query(...),
username: str = Query(...),
password: str = Query(...)
):
try:
device = CMTS_DEVICE.copy()
device.update({'host': host, 'username': username, 'password': password})
net_connect = ConnectHandler(**device)
raw_output = str(net_connect.send_command("show version", read_timeout=15))
net_connect.disconnect()
import re
# 🌟 使用 Regex 尋找 infra 或 vcmts-cd-0 後面的版本號
match = re.search(r"(?:infra|vcmts-cd-0)\s+([\w\.\-]+)", raw_output)
if match:
return {"status": "success", "version": match.group(1)}
else:
return {"status": "error", "message": "無法解析版本號", "raw_output": raw_output}
except Exception as e:
return {"status": "error", "message": f"CMTS Connection Error: {str(e)}"}

View File

@ -61,6 +61,23 @@ export async function apiClearCache(paths) {
return response.json();
}
export async function apiGetCmtsVersion(host, username, password) {
const url = `/api/v1/cmts-version?host=${encodeURIComponent(host)}&username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`;
const response = await fetch(url);
return response.json();
}
export async function apiGetScanStatus() {
try {
const response = await fetch('/api/v1/scan-status');
const data = await response.json();
return data.is_scanning;
} catch (e) {
console.warn("無法取得掃描狀態,預設為未掃描", e);
return false;
}
}
// ==========================================
// 3. 指令生成與執行 (CLI Generation & Execution)
// ==========================================

View File

@ -11,7 +11,8 @@ import { initTerminal, connectWebSocket, disconnectWebSocket, injectCommand, dow
import {
apiGetFullConfig, apiClearCache, apiExecuteQuery,
apiGetTreeFilters, apiSaveTreeFilters,
apiSyncLeafOptionsStream
apiSyncLeafOptionsStream, apiGetCmtsVersion,
apiGetScanStatus
} from './api.js';
// 3. 共用工具模組 (Utils)
@ -213,12 +214,56 @@ async function fetchFullConfig() {
if (loadingMsg) loadingMsg.style.display = 'inline-block';
if (treeContainer) treeContainer.innerHTML = '';
let needAutoScan = false; // 🌟 標記是否需要自動掃描
try {
// --- 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. 載入完整配置 ---
const result = await apiGetFullConfig(connInfo.host, connInfo.user, connInfo.pass);
if (result.status === 'success') {
if (treeContainer) {
treeContainer.innerHTML = buildTree(result.data);
enableGlobalScanButton();
// 🌟 3. 檢查後端是否已經有人在掃描
const isSomeoneScanning = await apiGetScanStatus();
if (isSomeoneScanning) {
// B 使用者情境:有人在掃描了,反灰按鈕並提示
alert("💡 提示:系統正在背景更新設備選項快取,部分選單題示內容可能稍後才會顯示完整。");
lockScanButton(60000); // 使用您現有的鎖定 UI 函數
} else {
// A 使用者情境:沒人在掃描,正常顯示按鈕
enableGlobalScanButton();
// 如果剛清空快取,自動啟動掃描 (改為全域掃描)
if (needAutoScan) {
setTimeout(() => {
scanGlobalMissingOptions();
}, 500); // 給予 0.5 秒 UI 渲染緩衝
}
}
}
} else {
if (treeContainer) treeContainer.innerHTML = `<div style="color: #c0392b; font-weight: bold; margin: 20px;">❌ 錯誤: ${result.message}</div>`;
@ -230,7 +275,6 @@ async function fetchFullConfig() {
}
}
// ============================================================================
// 🌟 4. 選項快取與背景掃描 (Cache & Background Scanning)
// ============================================================================
@ -243,11 +287,8 @@ async function enhanceInputWithOptions(path, elementId) {
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)) {
if (leafCache && leafCache.options && leafCache.options.length > 0) {
const container = document.getElementById(`container-${elementId}`);
const input = container.querySelector('.edit-input');
@ -275,101 +316,79 @@ async function enhanceInputWithOptions(path, elementId) {
}
}
// 一鍵掃描缺失選項 (包含快取比對 + 防連點保護 + 僅掃描可見項目)
async function scanAllMissingOptions() {
const btn = document.getElementById('global-scan-btn');
const statusEl = document.getElementById('scan-status');
const treeContainer = document.getElementById('tree-container');
// ============================================================================
// 🌟 核心功能:執行掃描 (支援局部 / 全域)
// ============================================================================
async function performScan(isGlobal) {
const isSomeoneScanning = await apiGetScanStatus();
if (isSomeoneScanning) {
alert("目前已有其他使用者正在執行掃描,請稍候共享更新結果!");
return;
}
const treeContainer = document.getElementById('tree-container');
if (!treeContainer) return;
const statusEl = document.getElementById('scan-status');
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 = "⏳ 掃描冷卻中...";
}
// 鎖定所有 4 顆按鈕
setAdminButtonsState(true, "⏳ 掃描冷卻中...");
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 (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);
}
}
}
});
if (pathsToSync.length === 0) {
statusEl.textContent = "✅ 當前展開的欄位都已有最新選項,無需掃描!";
statusEl.textContent = isGlobal ? "✅ 全域所有欄位都已有最新選項,無需掃描!" : "✅ 局部展開的欄位都已有最新選項,無需掃描!";
statusEl.style.color = "#27ae60";
setTimeout(() => statusEl.textContent = "", 3000);
if (btn) {
btn.disabled = false;
btn.style.backgroundColor = "";
btn.style.cursor = "pointer";
btn.innerHTML = "🔍 掃描畫面缺失選項";
}
setAdminButtonsState(false);
localStorage.removeItem('scanLockUntil');
return;
}
statusEl.innerHTML = `⏳ 正在建立即時連線...`;
statusEl.innerHTML = `⏳ 正在建立即時連線... (共需掃描 ${pathsToSync.length} 個項目)`;
statusEl.style.color = "#e67e22";
// 🌟 核心修改:改用串流 API並傳入進度更新的回呼函數
await apiSyncLeafOptionsStream(pathsToSync, (data) => {
if (data.event === 'progress') {
// 收到進度:即時更新畫面數字與當前路徑
statusEl.innerHTML = `⏳ 背景掃描進度:<b>${data.current} / ${data.total}</b> (正在處理: ${data.current_path})`;
statusEl.style.color = "#f39c12";
}
else if (data.event === 'done') {
// 掃描完成:恢復按鈕狀態並顯示成功
statusEl.innerHTML = `✅ 掃描完美達成!快取已全面更新。`;
statusEl.style.color = "#27ae60";
if (btn) {
btn.disabled = false;
btn.style.backgroundColor = "#8e44ad";
btn.style.cursor = "pointer";
btn.innerText = "🔍 掃描畫面缺失選項";
}
setAdminButtonsState(false);
localStorage.removeItem('scanLockUntil');
setTimeout(() => statusEl.textContent = "", 5000);
}
else if (data.event === 'error') {
// 發生錯誤:顯示紅字並恢復按鈕
statusEl.innerHTML = `❌ 錯誤: ${data.message}`;
statusEl.style.color = "#e74c3c";
if (btn) {
btn.disabled = false;
btn.style.backgroundColor = "#8e44ad";
btn.style.cursor = "pointer";
btn.innerText = "🔍 掃描畫面缺失選項";
}
setAdminButtonsState(false);
localStorage.removeItem('scanLockUntil');
}
});
@ -378,37 +397,77 @@ async function scanAllMissingOptions() {
console.error("掃描請求失敗:", error);
statusEl.textContent = "❌ 掃描請求發送失敗,請檢查網路連線。";
statusEl.style.color = "#e74c3c";
setAdminButtonsState(false);
localStorage.removeItem('scanLockUntil');
}
}
// 清除畫面選項快取功能 (精準可見度判定版)
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);
}
// ============================================================================
// 🌟 核心功能:清除快取 (支援局部 / 全域)
// ============================================================================
async function performClear(isGlobal) {
const isSomeoneScanning = await apiGetScanStatus();
if (isSomeoneScanning) {
alert("⚠️ 系統正在進行全域選項掃描更新中!\n為避免資料衝突請稍候掃描完成再執行清除動作。");
lockScanButton(60000);
const statusEl = document.getElementById('scan-status');
if (statusEl) {
statusEl.innerHTML = `⏳ 其他使用者正在背景更新快取,請稍候...`;
statusEl.style.color = "#e67e22";
}
});
return;
}
if (pathsToClear.length === 0) return alert("畫面上沒有展開的選項可以清除!\n請先展開您想重抓的資料夾。");
if (!confirm(`確定要清除畫面上 ${pathsToClear.length} 個選項的快取嗎?\n(清除後請重新點擊「掃描畫面缺失選項」)`)) return;
let pathsToClear = [];
if (isGlobal) {
// 全域清除:直接向後端要所有的快取 Key
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("❌ 無法取得全域快取清單。");
}
} 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);
}
}
});
}
if (pathsToClear.length === 0) return alert(isGlobal ? "目前沒有任何快取可以清除!" : "局部沒有展開的選項可以清除!\n請先展開您想重抓的資料夾。");
const msg = isGlobal
? `確定要清除「全域」共 ${pathsToClear.length} 個選項的快取嗎?\n(這將會清空所有已儲存的下拉選單資料)`
: `確定要清除局部 ${pathsToClear.length} 個選項的快取嗎?\n(清除後請重新點擊「掃描局部缺失選項」)`;
if (!confirm(msg)) return;
try {
const result = await apiClearCache(pathsToClear);
alert(`✅ 成功清除 ${result.cleared_count} 筆快取!\n現在請點擊旁邊的「掃描畫面缺失選項」來重新擷取。`);
alert(`✅ 成功清除 ${result.cleared_count} 筆快取!`);
} catch (error) {
console.error("清除快取失敗:", error);
alert("❌ 清除快取失敗,請檢查網路連線或後端 API 是否正確啟動。");
}
}
// 綁定給 HTML 呼叫的 4 個獨立函數
function scanVisibleMissingOptions() { performScan(false); }
function scanGlobalMissingOptions() { performScan(true); }
function clearVisibleCache() { performClear(false); }
function clearGlobalCache() { performClear(true); }
// ============================================================================
// 🌟 5. 系統設定與過濾器 (System Settings & Filters)
@ -524,49 +583,60 @@ function closeModal(event) {
document.addEventListener("DOMContentLoaded", checkScanButtonState);
// ==========================================
// 🛡️ 系統管理員雙重解鎖邏輯 (URL + 彩蛋)
// 🛡️ 系統管理員雙重解鎖與 4 顆按鈕連動邏輯
// ==========================================
const ADMIN_BTN_IDS = ['btn-scan-visible', 'btn-clear-visible', 'btn-scan-global', 'btn-clear-global'];
document.addEventListener('DOMContentLoaded', () => {
const scanBtn = document.getElementById('global-scan-btn');
const clearBtn = document.getElementById('clearCacheBtn');
const appTitle = document.getElementById('app-title');
// 定義解鎖動作:把按鈕顯示出來
function unlockAdminFeatures() {
if (scanBtn) scanBtn.style.display = 'inline-block';
if (clearBtn) clearBtn.style.display = 'inline-block';
ADMIN_BTN_IDS.forEach(id => {
const btn = document.getElementById(id);
if (btn) btn.style.display = 'inline-block';
});
}
// 方案一URL 隱藏密鑰檢查 (?mode=godmode)
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('mode') === 'godmode') {
unlockAdminFeatures();
}
if (urlParams.get('mode') === 'godmode') unlockAdminFeatures();
// 方案二:彩蛋解鎖 (對標題連點 5 次)
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);
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('global-scan-btn');
const btn = document.getElementById('btn-scan-visible');
if (!btn) return;
const scanLockUntil = localStorage.getItem('scanLockUntil');
if (scanLockUntil && Date.now() < parseInt(scanLockUntil)) {
@ -575,35 +645,22 @@ function checkScanButtonState() {
}
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 = "⏳ 背景掃描執行中...";
setAdminButtonsState(true, "⏳ 背景掃描執行中...");
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 = "⏳ 請先載入樹狀圖";
setAdminButtonsState(true, "⏳ 請先載入樹狀圖");
}
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 = "🔍 掃描畫面缺失選項";
setAdminButtonsState(false);
}
}
@ -640,8 +697,10 @@ window.loadMacDomainList = loadMacDomainList;
window.openSystemSettings = openSystemSettings;
window.loadRealConfigForFilters = loadRealConfigForFilters;
window.saveSystemSettings = saveSystemSettings;
window.scanVisibleMissingOptions = scanVisibleMissingOptions;
window.scanGlobalMissingOptions = scanGlobalMissingOptions;
window.clearVisibleCache = clearVisibleCache;
window.scanAllMissingOptions = scanAllMissingOptions;
window.clearGlobalCache = clearGlobalCache;
// --- 樹狀圖展開與編輯操作 ---
window.expandAll = expandAll;

View File

@ -87,8 +87,6 @@ export async function startEditFolder(path, elementId) {
try { optData = await apiGetLeafOptions(); } catch (e) { console.warn("無法取得選項快取", e); }
const pathsToSync = [];
const currentTime = Math.floor(Date.now() / 1000);
const CACHE_TTL = 86400 * 7;
leafContainers.forEach(container => {
const origVal = container.getAttribute('data-original');
@ -105,7 +103,7 @@ export async function startEditFolder(path, elementId) {
let leafCache = optData[cacheKey];
if (!leafCache || (currentTime - leafCache.updated_at >= CACHE_TTL)) {
if (!leafCache) {
pathsToSync.push(cacheKey);
}
@ -179,10 +177,8 @@ export async function startEditLeaf(path, elementId) {
}
let leafCache = optData[cacheKey];
const currentTime = Math.floor(Date.now() / 1000);
const CACHE_TTL = 86400 * 7;
if (!leafCache || (currentTime - leafCache.updated_at >= CACHE_TTL)) {
if (!leafCache) {
apiSyncLeafOptions([cacheKey]);
let found = false;
for (let i = 0; i < 10; i++) {

View File

@ -1,5 +1,87 @@
/* --- static/style.css --- */
/* =========================================
全域按鈕色彩系統 (Modern Enterprise Vibe)
========================================= */
:root {
/* 基礎設定 */
--btn-radius: 6px;
--btn-transition: all 0.2s ease-in-out;
/* 色彩定義 */
--color-emerald: #059669; /* 連線 */
--color-emerald-hover: #047857;
--color-slate: #64748B; /* 斷開/中性/取消 */
--color-slate-hover: #475569;
--color-blue: #2563EB; /* 載入/查詢 */
--color-blue-hover: #1D4ED8;
--color-indigo: #4F46E5; /* 儲存/套用 */
--color-indigo-hover: #4338CA;
--color-violet: #7C3AED; /* 掃描/自動化 */
--color-violet-hover: #6D28D9;
--color-rose: #DC2626; /* 清除/刪除 */
--color-rose-hover: #B91C1C;
--color-disabled-bg: #E2E8F0;
--color-disabled-text: #94A3B8;
--color-busy-bg: #F59E0B; /* 忙碌中(橘黃) */
}
/* 基礎按鈕共用樣式 */
.btn-modern {
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
border: none;
border-radius: var(--btn-radius);
color: #FFFFFF;
cursor: pointer;
transition: var(--btn-transition);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
/* 禁用狀態 (所有按鈕共用) */
.btn-modern:disabled {
background-color: var(--color-disabled-bg) !important;
color: var(--color-disabled-text) !important;
cursor: not-allowed !important;
box-shadow: none;
}
/* 各別語意 Class */
.btn-connect { background-color: var(--color-emerald); }
.btn-connect:hover:not(:disabled) { background-color: var(--color-emerald-hover); }
.btn-disconnect { background-color: var(--color-slate); }
.btn-disconnect:hover:not(:disabled) { background-color: var(--color-slate-hover); }
.btn-load { background-color: var(--color-blue); }
.btn-load:hover:not(:disabled) { background-color: var(--color-blue-hover); }
.btn-save { background-color: var(--color-indigo); }
.btn-save:hover:not(:disabled) { background-color: var(--color-indigo-hover); }
.btn-scan { background-color: var(--color-violet); }
.btn-scan:hover:not(:disabled) { background-color: var(--color-violet-hover); }
.btn-clear { background-color: var(--color-rose); }
.btn-clear:hover:not(:disabled) { background-color: var(--color-rose-hover); }
/* 特殊忙碌狀態 (給掃描或載入時使用) */
.btn-modern.is-busy:disabled {
background-color: var(--color-busy-bg) !important;
color: #FFFFFF !important;
}
/* 1. 基礎滿版佈局 (全域 80% 縮放感) */
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 0; padding: 15px 20px; box-sizing: border-box; background-color: #f5f7fa; height: 100vh; display: flex; flex-direction: column; font-size: 14px; }
h1 { color: #2c3e50; margin-top: 0; flex-shrink: 0; margin-bottom: 12px; font-size: 24px; }

View File

@ -1,7 +1,3 @@
{
"hidden_keys": [
"ssh",
"hostname",
"clock"
]
"hidden_keys": []
}