feat: 1.重構快取掃描邏輯,新增局部與全域操作分離, 2.GUI按鈕格規格統一化。
This commit is contained in:
parent
682c9341a7
commit
03814b29dc
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -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}")
|
||||
|
||||
|
|
|
|||
49
index.html
49
index.html
|
|
@ -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>
|
||||
|
||||
<!-- 表單 3:MAC 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="關閉面板">×</span>
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}"}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
// ==========================================
|
||||
|
|
|
|||
243
static/app.js
243
static/app.js
|
|
@ -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);
|
||||
|
||||
// 🌟 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)
|
||||
// ============================================================================
|
||||
|
|
@ -244,10 +288,7 @@ 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;
|
||||
|
||||
// 如果是全域掃描,或是該節點沒有被收合(局部可見),就納入檢查
|
||||
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 || (currentTime - leafCache.updated_at >= CACHE_TTL)) {
|
||||
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,14 +397,42 @@ async function scanAllMissingOptions() {
|
|||
console.error("掃描請求失敗:", error);
|
||||
statusEl.textContent = "❌ 掃描請求發送失敗,請檢查網路連線。";
|
||||
statusEl.style.color = "#e74c3c";
|
||||
setAdminButtonsState(false);
|
||||
localStorage.removeItem('scanLockUntil');
|
||||
}
|
||||
}
|
||||
|
||||
// 清除畫面選項快取功能 (精準可見度判定版)
|
||||
async function clearVisibleCache() {
|
||||
// ============================================================================
|
||||
// 🌟 核心功能:清除快取 (支援局部 / 全域)
|
||||
// ============================================================================
|
||||
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;
|
||||
}
|
||||
|
||||
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');
|
||||
const pathsToClear = [];
|
||||
|
||||
elements.forEach(el => {
|
||||
const isHiddenByFolder = el.closest('details:not([open])') !== null;
|
||||
if (!isHiddenByFolder) {
|
||||
|
|
@ -396,19 +443,31 @@ async function clearVisibleCache() {
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (pathsToClear.length === 0) return alert("畫面上沒有展開的選項可以清除!\n請先展開您想重抓的資料夾。");
|
||||
if (!confirm(`確定要清除畫面上 ${pathsToClear.length} 個選項的快取嗎?\n(清除後請重新點擊「掃描畫面缺失選項」)`)) return;
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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++) {
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
{
|
||||
"hidden_keys": [
|
||||
"ssh",
|
||||
"hostname",
|
||||
"clock"
|
||||
]
|
||||
"hidden_keys": []
|
||||
}
|
||||
Loading…
Reference in New Issue