diff --git a/all_code.txt b/all_code.txt index 6bd585a..8bd3b76 100644 --- a/all_code.txt +++ b/all_code.txt @@ -2454,7 +2454,7 @@ function resetAllManagerData() { // 🌟 3. 設備查詢與全域配置載入 (Query & Full Config) // ============================================================================ -// 執行綜合查詢 (CM / RPD) +// 執行綜合查詢 (CM / RPD) - 🌟 導入前端模擬動態訊息流 async function executeQuery(mode) { if (!isConnected) return alert("請先點擊畫面上方的「連線至 CMTS」按鈕!"); @@ -2467,10 +2467,28 @@ async function executeQuery(mode) { openModal(connInfo.host); const outputEl = document.getElementById('modalOutput'); - outputEl.innerHTML = `正在向 ${connInfo.host} 查詢中,請稍候...`; + + // 🌟 模擬動態訊息流邏輯 + const states = [ + `🔌 正在建立 SSH 連線至 ${connInfo.host}...`, + `📥 正在向設備發送查詢指令...`, + `🧩 正在等待設備回傳資料...` + ]; + let progressIdx = 0; + outputEl.innerHTML = `${states[0]}`; + + const progressInterval = setInterval(() => { + progressIdx++; + if (progressIdx < states.length) { + const textEl = document.getElementById('query-progress-text'); + if (textEl) textEl.innerText = states[progressIdx]; + } + }, 1500); // 每 1.5 秒切換一次狀態 try { const result = await apiExecuteQuery(queryType, target, connInfo.host, connInfo.user, connInfo.pass); + clearInterval(progressInterval); // 收到結果,停止輪播 + if (result.status === 'success') { outputEl.style.color = "#e0e0e0"; outputEl.textContent = isDebug ? `[DEBUG] 實際發送指令: ${result.command}\n==================================================\n${result.data}` : result.data; @@ -2479,6 +2497,7 @@ async function executeQuery(mode) { outputEl.textContent = `查詢失敗:\n${result.detail || result.message}`; } } catch (error) { + clearInterval(progressInterval); // 發生錯誤,停止輪播 outputEl.style.color = "#e74c3c"; outputEl.textContent = `連線失敗: ${error}`; } @@ -2494,6 +2513,7 @@ async function fetchFullConfig(configType = 'running') { if (loadingMsg) { loadingMsg.style.display = 'inline-block'; + loadingMsg.style.color = '#f39c12'; // 🌟 確保初始狀態為橘色 loadingMsg.innerHTML = '⏳ 準備連線至設備...'; } if (treeContainer) treeContainer.innerHTML = ''; @@ -2502,7 +2522,6 @@ async function fetchFullConfig(configType = 'running') { let progressInterval = null; try { - // --- 1. 版本守門員 (維持原樣) --- try { const versionRes = await apiGetCmtsVersion(connInfo.host, connInfo.user, connInfo.pass); if (versionRes.status === 'success') { @@ -2524,7 +2543,6 @@ async function fetchFullConfig(configType = 'running') { console.warn("版本檢查失敗,略過", vErr); } - // --- 2. 載入完整配置 (ReadableStream 串流接收) --- const response = await fetch(`/api/v1/cmts-full-config/stream?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}&config_type=${configType}`); if (!response.ok) throw new Error(`伺服器回應錯誤: ${response.status}`); @@ -2559,26 +2577,20 @@ async function fetchFullConfig(configType = 'running') { } }, 500); } else { - // 🌟 魔法核心:當收到下一個狀態時,先平順收尾動畫 if (progressInterval) { clearInterval(progressInterval); progressInterval = null; - - // 1. 強制填滿 100% 並給予成功提示 if (loadingMsg) { loadingMsg.innerHTML = `📥 正在下載 ${configType} 配置檔 (100%) - 下載完成!`; - loadingMsg.style.color = "#27ae60"; // 瞬間變綠色增加爽感 + loadingMsg.style.color = "#27ae60"; } - - // 2. 刻意停頓 0.6 秒,讓大腦接收視覺回饋 await new Promise(resolve => setTimeout(resolve, 600)); - - // 3. 恢復原本的顏色 - if (loadingMsg) loadingMsg.style.color = ""; + if (loadingMsg) loadingMsg.style.color = "#f39c12"; // 🌟 恢復橘色 + } + if (loadingMsg) { + loadingMsg.style.color = "#f39c12"; // 🌟 確保橘色 + loadingMsg.innerHTML = data.message; } - - // 停頓結束後,才顯示新的狀態 (例如:🧩 正在解析配置...) - if (loadingMsg) loadingMsg.innerHTML = data.message; } } else if (data.status === 'success') { @@ -2597,7 +2609,6 @@ async function fetchFullConfig(configType = 'running') { window.currentTreeData = finalTreeData; - // --- 3. 防呆攔截與畫面渲染 --- const currentSelectedTask = document.getElementById('configTask').value; const expectedTask = configType === 'running' ? 'form-running-config' : 'form-full-config'; @@ -2625,7 +2636,7 @@ async function fetchFullConfig(configType = 'running') { if (progressInterval) clearInterval(progressInterval); if (loadingMsg) { loadingMsg.style.display = 'none'; - loadingMsg.style.color = ""; // 確保重置顏色 + loadingMsg.style.color = "#f39c12"; // 🌟 確保重置為橘色 } } } @@ -2888,12 +2899,11 @@ async function openSystemSettings() { } } -// 載入真實設備配置以供過濾器勾選 +// 載入真實設備配置以供過濾器勾選 - 🌟 升級為真實動態訊息流 (重用現有串流 API) async function loadRealConfigForFilters() { const connInfo = getGlobalConnectionInfo(); if (!connInfo) return alert("請先在畫面上方輸入設備連線資訊!"); - // 🌟 動態抓取過濾器模式 const modeSelect = document.getElementById('filter-mode-select'); const mode = modeSelect ? modeSelect.value : 'running'; @@ -2901,26 +2911,89 @@ async function loadRealConfigForFilters() { const container = document.getElementById('filter-checkboxes'); loadingMsg.style.display = 'inline-block'; + loadingMsg.style.color = '#f39c12'; // 🌟 確保橘色 + loadingMsg.innerHTML = '⏳ 準備連線至設備...'; container.style.display = 'none'; - try { - // 🌟 根據模式載入對應的配置檔 (Running 或 Full) - const url = `/api/v1/cmts-full-config?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}&skip_filter=true&config_type=${mode}`; - const response = await fetch(url); - const result = await response.json(); + let progressInterval = null; // 🌟 新增:進度條計時器 - if (result.status === 'success') { - container.style.display = 'block'; - // 🌟 傳遞 mode 給 buildRealFilterTree - container.innerHTML = buildRealFilterTree(result.data, "", currentHiddenKeys, false, mode); - } else { - container.style.display = 'block'; - container.innerHTML = `
❌ 錯誤: ${result.message}
`; + try { + const url = `/api/v1/cmts-full-config/stream?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}&skip_filter=true&config_type=${mode}`; + const response = await fetch(url); + + if (!response.ok) throw new Error(`伺服器回應錯誤: ${response.status}`); + + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let buffer = ""; + let finalTreeData = null; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + let lines = buffer.split('\n'); + buffer = lines.pop(); + + for (let line of lines) { + if (!line.trim()) continue; + try { + const data = JSON.parse(line); + + if (data.status === 'progress') { + // 🌟 導入百分比動畫邏輯 + if (data.message.includes('正在下載')) { + let progress = 0; + if (progressInterval) clearInterval(progressInterval); + + progressInterval = setInterval(() => { + progress += (95 - progress) * 0.06; + if (loadingMsg) { + loadingMsg.innerHTML = `📥 正在下載 ${mode} 配置檔 (${Math.round(progress)}%)...`; + } + }, 500); + } else { + if (progressInterval) { + clearInterval(progressInterval); + progressInterval = null; + if (loadingMsg) { + loadingMsg.innerHTML = `📥 正在下載 ${mode} 配置檔 (100%) - 下載完成!`; + loadingMsg.style.color = "#27ae60"; // 瞬間變綠色 + } + await new Promise(resolve => setTimeout(resolve, 600)); + if (loadingMsg) loadingMsg.style.color = "#f39c12"; // 恢復橘色 + } + if (loadingMsg) { + loadingMsg.style.color = '#f39c12'; + loadingMsg.innerHTML = data.message; + } + } + } + else if (data.status === 'success') { + finalTreeData = data.data; + } + else if (data.status === 'error') { + throw new Error(data.message); + } + } catch (e) { + console.warn("解析串流 JSON 失敗:", line); + } + } } + + if (finalTreeData) { + container.style.display = 'block'; + container.innerHTML = buildRealFilterTree(finalTreeData, "", currentHiddenKeys, false, mode); + } else { + throw new Error("未收到完整的配置資料"); + } + } catch (error) { container.style.display = 'block'; - container.innerHTML = `
❌ 連線失敗: ${error.message}
`; + container.innerHTML = `
❌ 連線或解析失敗: ${error.message}
`; } finally { + if (progressInterval) clearInterval(progressInterval); // 🌟 確保清除計時器 loadingMsg.style.display = 'none'; } } @@ -2980,9 +3053,8 @@ async function saveSystemSettings() { // 🌟 新增:全域變數用來暫存所有快照資料,實現前端秒速過濾 let allBackupsData = []; -// 1. 建立新快照 +// 1. 建立新快照 - 🌟 升級為真實動態訊息流 (含百分比動畫) async function createSnapshot() { - // 🛡️ 防線一:必須先連線才能備份 if (!isConnected) { return alert("⚠️ 請先點擊畫面上方的「連線至 CMTS」成功後,再執行備份任務!\n(確保您清楚目前正在操作哪一台設備)"); } @@ -2993,7 +3065,6 @@ async function createSnapshot() { const snapshotName = document.getElementById('snapshotName').value.trim(); if (!snapshotName) return alert("請輸入快照名稱!"); - // 🟢 抓取描述輸入框的值 const snapshotDescription = document.getElementById('snapshotDescription')?.value.trim() || ""; const configType = document.getElementById('backupConfigType').value; const btn = document.getElementById('btnCreateSnapshot'); @@ -3001,6 +3072,10 @@ async function createSnapshot() { btn.disabled = true; msg.style.display = 'inline-block'; + msg.style.color = '#f39c12'; // 🌟 確保橘色 + msg.innerHTML = '⏳ 準備連線至設備...'; + + let progressInterval = null; // 🌟 新增:進度條計時器 try { const response = await fetch('/api/v1/backups/snapshot', { @@ -3011,25 +3086,84 @@ async function createSnapshot() { username: connInfo.user, password: connInfo.pass, snapshot_name: snapshotName, - description: snapshotDescription, // 🟢 將描述傳給後端 + description: snapshotDescription, config_type: configType }) }); - const result = await response.json(); - - if (result.status === 'success') { - alert(`✅ ${result.message}`); - document.getElementById('snapshotName').value = ''; // 清空名稱輸入框 - if (document.getElementById('snapshotDescription')) { - document.getElementById('snapshotDescription').value = ''; // 🟢 清空描述輸入框 + + if (!response.ok) throw new Error(`伺服器回應錯誤: ${response.status}`); + + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let buffer = ""; + let isSuccess = false; + let finalMessage = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + let lines = buffer.split('\n'); + buffer = lines.pop(); + + for (let line of lines) { + if (!line.trim()) continue; + try { + const data = JSON.parse(line); + if (data.status === 'progress') { + // 🌟 導入百分比動畫邏輯 + if (data.message.includes('正在下載')) { + let progress = 0; + if (progressInterval) clearInterval(progressInterval); + + progressInterval = setInterval(() => { + progress += (95 - progress) * 0.06; + if (msg) { + msg.innerHTML = `📥 正在下載 ${configType} 配置檔 (${Math.round(progress)}%)...`; + } + }, 500); + } else { + if (progressInterval) { + clearInterval(progressInterval); + progressInterval = null; + if (msg) { + msg.innerHTML = `📥 正在下載 ${configType} 配置檔 (100%) - 下載完成!`; + msg.style.color = "#27ae60"; // 瞬間變綠色 + } + await new Promise(resolve => setTimeout(resolve, 600)); + if (msg) msg.style.color = "#f39c12"; // 恢復橘色 + } + if (msg) { + msg.style.color = '#f39c12'; + msg.innerHTML = data.message; + } + } + } else if (data.status === 'success') { + isSuccess = true; + finalMessage = data.message; + } else if (data.status === 'error') { + throw new Error(data.message); + } + } catch (e) { + if (e.message) throw e; + console.warn("解析串流 JSON 失敗:", line); + } } - loadBackupHistory(); // 重新載入歷史紀錄 - } else { - alert(`❌ 備份失敗: ${result.message}`); + } + + if (isSuccess) { + alert(`✅ ${finalMessage}`); + document.getElementById('snapshotName').value = ''; + if (document.getElementById('snapshotDescription')) { + document.getElementById('snapshotDescription').value = ''; + } + loadBackupHistory(); } } catch (error) { alert(`❌ 發生錯誤: ${error.message}`); } finally { + if (progressInterval) clearInterval(progressInterval); // 🌟 確保清除計時器 btn.disabled = false; msg.style.display = 'none'; } @@ -3044,7 +3178,8 @@ async function loadBackupHistory() { const tbody = document.getElementById('backup-history-tbody'); if (!tbody) return; - tbody.innerHTML = '⏳ 正在載入歷史紀錄...'; + // 🌟 修正:將原本的灰色改為橘色並加粗 + tbody.innerHTML = '⏳ 正在載入歷史紀錄...'; try { // 🌟 關鍵修改:將 config_type 改為 'all',一次把該設備的所有快照撈回來 @@ -3203,8 +3338,9 @@ async function restoreBackup(snapshotId, snapshotName, backupHost) { // 1. 移除進度條,改為純文字百分比動態顯示 modalTargetInfo.innerText = `正在分析快照差異: ${snapshotName}`; + // 🌟 修正:將原本的藍色改為橘色 modalOutput.innerHTML = ` -
🔍 正在抓取設備當前配置,並與快照進行深度比對... (0%)
+
🔍 正在抓取設備當前配置,並與快照進行深度比對... (0%)
`; modal.classList.add('active'); @@ -4338,7 +4474,8 @@ export async function startEditLeaf(path, elementId) { const origVal = container.getAttribute('data-original'); const safeOrigVal = escapeHTML(origVal); - container.innerHTML = `⏳ 設備連線與載入選項中...`; + // 🌟 修正:統一為標準橘色並加粗 (原為 #e67e22) + container.innerHTML = `⏳ 設備連線與載入選項中...`; try { let optData = await apiGetLeafOptions(host, currentMode); @@ -4770,7 +4907,7 @@ async function applyEditLeaf(path, elementId) { export function executeSideCLI() { const finalScript = document.getElementById('side-cli-textarea').value; document.getElementById('side-pane-title').innerHTML = '⏳ 正在寫入設備...'; - document.getElementById('side-pane-title').style.color = '#3498db'; + document.getElementById('side-pane-title').style.color = '#f39c12'; // 🌟 統一載入中狀態為橘色 (原為 #3498db) document.getElementById('btn-side-cancel').style.display = 'none'; document.getElementById('btn-side-confirm').style.display = 'none'; document.getElementById('btn-side-close').style.display = 'none'; @@ -6044,35 +6181,51 @@ async def delete_backup(backup_id: str): return {"status": "error", "message": "刪除失敗或找不到該筆資料"} return {"status": "success", "message": "快照已成功刪除"} -@router.post("/snapshot", summary="手動建立設備快照") +@router.post("/snapshot", summary="手動建立設備快照 (串流版)") async def create_snapshot(req: SnapshotRequest): - try: - raw_cli = await fetch_raw_config(req.host, req.username, req.password, req.config_type) - parsed_tree = parse_cli_to_tree(raw_cli) + async def backup_streamer(): + try: + # 1. 廣播:正在連線 + yield json.dumps({"status": "progress", "message": f"🔌 正在建立 SSH 連線至 {req.host}..."}) + "\n" + await asyncio.sleep(0.1) - backup_id = await insert_config_backup( - host=req.host, - config_type=req.config_type, - raw_cli=raw_cli, - parsed_tree=parsed_tree, - snapshot_name=req.snapshot_name, - description=req.description, # 🟢 將描述傳遞給資料庫函數 - is_auto=False - ) + # 2. 廣播:正在下載 + yield json.dumps({"status": "progress", "message": f"📥 正在下載 {req.config_type} 配置檔 (可能需要 30~60 秒)..."}) + "\n" + raw_cli = await fetch_raw_config(req.host, req.username, req.password, req.config_type) - if not backup_id: - return {"status": "error", "message": "資料庫寫入失敗,請檢查系統日誌"} + # 3. 廣播:正在解析 + yield json.dumps({"status": "progress", "message": "🧩 正在解析配置並建構樹狀圖結構..."}) + "\n" + parsed_tree = await asyncio.to_thread(parse_cli_to_tree, raw_cli) - return { - "status": "success", - "message": f"快照 '{req.snapshot_name}' 建立成功!", - "backup_id": backup_id, - "host": req.host - } + # 4. 廣播:寫入資料庫 + yield json.dumps({"status": "progress", "message": "💾 正在將快照寫入資料庫..."}) + "\n" + backup_id = await insert_config_backup( + host=req.host, + config_type=req.config_type, + raw_cli=raw_cli, + parsed_tree=parsed_tree, + snapshot_name=req.snapshot_name, + description=req.description, + is_auto=False + ) - except Exception as e: - logger.error(f"❌ 建立快照失敗: {e}") - return {"status": "error", "message": f"設備連線或解析失敗: {str(e)}"} + if not backup_id: + yield json.dumps({"status": "error", "message": "資料庫寫入失敗,請檢查系統日誌"}) + "\n" + return + + # 5. 廣播:成功 + yield json.dumps({ + "status": "success", + "message": f"快照 '{req.snapshot_name}' 建立成功!", + "backup_id": backup_id, + "host": req.host + }) + "\n" + + except Exception as e: + logger.error(f"❌ 建立快照失敗: {e}") + yield json.dumps({"status": "error", "message": f"設備連線或解析失敗: {str(e)}"}) + "\n" + + return StreamingResponse(backup_streamer(), media_type="application/x-ndjson") @router.post("/{backup_id}/diff", summary="分析設備當前配置與快照的差異") async def analyze_backup_diff(backup_id: str, req: RestoreRequest): diff --git a/routers/backup.py b/routers/backup.py index a9b6ab8..5028794 100644 --- a/routers/backup.py +++ b/routers/backup.py @@ -149,35 +149,51 @@ async def delete_backup(backup_id: str): return {"status": "error", "message": "刪除失敗或找不到該筆資料"} return {"status": "success", "message": "快照已成功刪除"} -@router.post("/snapshot", summary="手動建立設備快照") +@router.post("/snapshot", summary="手動建立設備快照 (串流版)") async def create_snapshot(req: SnapshotRequest): - try: - raw_cli = await fetch_raw_config(req.host, req.username, req.password, req.config_type) - parsed_tree = parse_cli_to_tree(raw_cli) + async def backup_streamer(): + try: + # 1. 廣播:正在連線 + yield json.dumps({"status": "progress", "message": f"🔌 正在建立 SSH 連線至 {req.host}..."}) + "\n" + await asyncio.sleep(0.1) - backup_id = await insert_config_backup( - host=req.host, - config_type=req.config_type, - raw_cli=raw_cli, - parsed_tree=parsed_tree, - snapshot_name=req.snapshot_name, - description=req.description, # 🟢 將描述傳遞給資料庫函數 - is_auto=False - ) + # 2. 廣播:正在下載 + yield json.dumps({"status": "progress", "message": f"📥 正在下載 {req.config_type} 配置檔 (可能需要 30~60 秒)..."}) + "\n" + raw_cli = await fetch_raw_config(req.host, req.username, req.password, req.config_type) - if not backup_id: - return {"status": "error", "message": "資料庫寫入失敗,請檢查系統日誌"} + # 3. 廣播:正在解析 + yield json.dumps({"status": "progress", "message": "🧩 正在解析配置並建構樹狀圖結構..."}) + "\n" + parsed_tree = await asyncio.to_thread(parse_cli_to_tree, raw_cli) - return { - "status": "success", - "message": f"快照 '{req.snapshot_name}' 建立成功!", - "backup_id": backup_id, - "host": req.host - } + # 4. 廣播:寫入資料庫 + yield json.dumps({"status": "progress", "message": "💾 正在將快照寫入資料庫..."}) + "\n" + backup_id = await insert_config_backup( + host=req.host, + config_type=req.config_type, + raw_cli=raw_cli, + parsed_tree=parsed_tree, + snapshot_name=req.snapshot_name, + description=req.description, + is_auto=False + ) - except Exception as e: - logger.error(f"❌ 建立快照失敗: {e}") - return {"status": "error", "message": f"設備連線或解析失敗: {str(e)}"} + if not backup_id: + yield json.dumps({"status": "error", "message": "資料庫寫入失敗,請檢查系統日誌"}) + "\n" + return + + # 5. 廣播:成功 + yield json.dumps({ + "status": "success", + "message": f"快照 '{req.snapshot_name}' 建立成功!", + "backup_id": backup_id, + "host": req.host + }) + "\n" + + except Exception as e: + logger.error(f"❌ 建立快照失敗: {e}") + yield json.dumps({"status": "error", "message": f"設備連線或解析失敗: {str(e)}"}) + "\n" + + return StreamingResponse(backup_streamer(), media_type="application/x-ndjson") @router.post("/{backup_id}/diff", summary="分析設備當前配置與快照的差異") async def analyze_backup_diff(backup_id: str, req: RestoreRequest): diff --git a/static/app.js b/static/app.js index c105017..92bc959 100644 --- a/static/app.js +++ b/static/app.js @@ -425,7 +425,7 @@ function resetAllManagerData() { // 🌟 3. 設備查詢與全域配置載入 (Query & Full Config) // ============================================================================ -// 執行綜合查詢 (CM / RPD) +// 執行綜合查詢 (CM / RPD) - 🌟 導入前端模擬動態訊息流 async function executeQuery(mode) { if (!isConnected) return alert("請先點擊畫面上方的「連線至 CMTS」按鈕!"); @@ -438,10 +438,28 @@ async function executeQuery(mode) { openModal(connInfo.host); const outputEl = document.getElementById('modalOutput'); - outputEl.innerHTML = `正在向 ${connInfo.host} 查詢中,請稍候...`; + + // 🌟 模擬動態訊息流邏輯 + const states = [ + `🔌 正在建立 SSH 連線至 ${connInfo.host}...`, + `📥 正在向設備發送查詢指令...`, + `🧩 正在等待設備回傳資料...` + ]; + let progressIdx = 0; + outputEl.innerHTML = `${states[0]}`; + + const progressInterval = setInterval(() => { + progressIdx++; + if (progressIdx < states.length) { + const textEl = document.getElementById('query-progress-text'); + if (textEl) textEl.innerText = states[progressIdx]; + } + }, 1500); // 每 1.5 秒切換一次狀態 try { const result = await apiExecuteQuery(queryType, target, connInfo.host, connInfo.user, connInfo.pass); + clearInterval(progressInterval); // 收到結果,停止輪播 + if (result.status === 'success') { outputEl.style.color = "#e0e0e0"; outputEl.textContent = isDebug ? `[DEBUG] 實際發送指令: ${result.command}\n==================================================\n${result.data}` : result.data; @@ -450,6 +468,7 @@ async function executeQuery(mode) { outputEl.textContent = `查詢失敗:\n${result.detail || result.message}`; } } catch (error) { + clearInterval(progressInterval); // 發生錯誤,停止輪播 outputEl.style.color = "#e74c3c"; outputEl.textContent = `連線失敗: ${error}`; } @@ -465,6 +484,7 @@ async function fetchFullConfig(configType = 'running') { if (loadingMsg) { loadingMsg.style.display = 'inline-block'; + loadingMsg.style.color = '#f39c12'; // 🌟 確保初始狀態為橘色 loadingMsg.innerHTML = '⏳ 準備連線至設備...'; } if (treeContainer) treeContainer.innerHTML = ''; @@ -473,7 +493,6 @@ async function fetchFullConfig(configType = 'running') { let progressInterval = null; try { - // --- 1. 版本守門員 (維持原樣) --- try { const versionRes = await apiGetCmtsVersion(connInfo.host, connInfo.user, connInfo.pass); if (versionRes.status === 'success') { @@ -495,7 +514,6 @@ async function fetchFullConfig(configType = 'running') { console.warn("版本檢查失敗,略過", vErr); } - // --- 2. 載入完整配置 (ReadableStream 串流接收) --- const response = await fetch(`/api/v1/cmts-full-config/stream?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}&config_type=${configType}`); if (!response.ok) throw new Error(`伺服器回應錯誤: ${response.status}`); @@ -530,26 +548,20 @@ async function fetchFullConfig(configType = 'running') { } }, 500); } else { - // 🌟 魔法核心:當收到下一個狀態時,先平順收尾動畫 if (progressInterval) { clearInterval(progressInterval); progressInterval = null; - - // 1. 強制填滿 100% 並給予成功提示 if (loadingMsg) { loadingMsg.innerHTML = `📥 正在下載 ${configType} 配置檔 (100%) - 下載完成!`; - loadingMsg.style.color = "#27ae60"; // 瞬間變綠色增加爽感 + loadingMsg.style.color = "#27ae60"; } - - // 2. 刻意停頓 0.6 秒,讓大腦接收視覺回饋 await new Promise(resolve => setTimeout(resolve, 600)); - - // 3. 恢復原本的顏色 - if (loadingMsg) loadingMsg.style.color = ""; + if (loadingMsg) loadingMsg.style.color = "#f39c12"; // 🌟 恢復橘色 + } + if (loadingMsg) { + loadingMsg.style.color = "#f39c12"; // 🌟 確保橘色 + loadingMsg.innerHTML = data.message; } - - // 停頓結束後,才顯示新的狀態 (例如:🧩 正在解析配置...) - if (loadingMsg) loadingMsg.innerHTML = data.message; } } else if (data.status === 'success') { @@ -568,7 +580,6 @@ async function fetchFullConfig(configType = 'running') { window.currentTreeData = finalTreeData; - // --- 3. 防呆攔截與畫面渲染 --- const currentSelectedTask = document.getElementById('configTask').value; const expectedTask = configType === 'running' ? 'form-running-config' : 'form-full-config'; @@ -596,7 +607,7 @@ async function fetchFullConfig(configType = 'running') { if (progressInterval) clearInterval(progressInterval); if (loadingMsg) { loadingMsg.style.display = 'none'; - loadingMsg.style.color = ""; // 確保重置顏色 + loadingMsg.style.color = "#f39c12"; // 🌟 確保重置為橘色 } } } @@ -859,12 +870,11 @@ async function openSystemSettings() { } } -// 載入真實設備配置以供過濾器勾選 +// 載入真實設備配置以供過濾器勾選 - 🌟 升級為真實動態訊息流 (重用現有串流 API) async function loadRealConfigForFilters() { const connInfo = getGlobalConnectionInfo(); if (!connInfo) return alert("請先在畫面上方輸入設備連線資訊!"); - // 🌟 動態抓取過濾器模式 const modeSelect = document.getElementById('filter-mode-select'); const mode = modeSelect ? modeSelect.value : 'running'; @@ -872,26 +882,89 @@ async function loadRealConfigForFilters() { const container = document.getElementById('filter-checkboxes'); loadingMsg.style.display = 'inline-block'; + loadingMsg.style.color = '#f39c12'; // 🌟 確保橘色 + loadingMsg.innerHTML = '⏳ 準備連線至設備...'; container.style.display = 'none'; - try { - // 🌟 根據模式載入對應的配置檔 (Running 或 Full) - const url = `/api/v1/cmts-full-config?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}&skip_filter=true&config_type=${mode}`; - const response = await fetch(url); - const result = await response.json(); + let progressInterval = null; // 🌟 新增:進度條計時器 - if (result.status === 'success') { - container.style.display = 'block'; - // 🌟 傳遞 mode 給 buildRealFilterTree - container.innerHTML = buildRealFilterTree(result.data, "", currentHiddenKeys, false, mode); - } else { - container.style.display = 'block'; - container.innerHTML = `
❌ 錯誤: ${result.message}
`; + try { + const url = `/api/v1/cmts-full-config/stream?host=${encodeURIComponent(connInfo.host)}&username=${encodeURIComponent(connInfo.user)}&password=${encodeURIComponent(connInfo.pass)}&skip_filter=true&config_type=${mode}`; + const response = await fetch(url); + + if (!response.ok) throw new Error(`伺服器回應錯誤: ${response.status}`); + + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let buffer = ""; + let finalTreeData = null; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + let lines = buffer.split('\n'); + buffer = lines.pop(); + + for (let line of lines) { + if (!line.trim()) continue; + try { + const data = JSON.parse(line); + + if (data.status === 'progress') { + // 🌟 導入百分比動畫邏輯 + if (data.message.includes('正在下載')) { + let progress = 0; + if (progressInterval) clearInterval(progressInterval); + + progressInterval = setInterval(() => { + progress += (95 - progress) * 0.06; + if (loadingMsg) { + loadingMsg.innerHTML = `📥 正在下載 ${mode} 配置檔 (${Math.round(progress)}%)...`; + } + }, 500); + } else { + if (progressInterval) { + clearInterval(progressInterval); + progressInterval = null; + if (loadingMsg) { + loadingMsg.innerHTML = `📥 正在下載 ${mode} 配置檔 (100%) - 下載完成!`; + loadingMsg.style.color = "#27ae60"; // 瞬間變綠色 + } + await new Promise(resolve => setTimeout(resolve, 600)); + if (loadingMsg) loadingMsg.style.color = "#f39c12"; // 恢復橘色 + } + if (loadingMsg) { + loadingMsg.style.color = '#f39c12'; + loadingMsg.innerHTML = data.message; + } + } + } + else if (data.status === 'success') { + finalTreeData = data.data; + } + else if (data.status === 'error') { + throw new Error(data.message); + } + } catch (e) { + console.warn("解析串流 JSON 失敗:", line); + } + } } + + if (finalTreeData) { + container.style.display = 'block'; + container.innerHTML = buildRealFilterTree(finalTreeData, "", currentHiddenKeys, false, mode); + } else { + throw new Error("未收到完整的配置資料"); + } + } catch (error) { container.style.display = 'block'; - container.innerHTML = `
❌ 連線失敗: ${error.message}
`; + container.innerHTML = `
❌ 連線或解析失敗: ${error.message}
`; } finally { + if (progressInterval) clearInterval(progressInterval); // 🌟 確保清除計時器 loadingMsg.style.display = 'none'; } } @@ -951,9 +1024,8 @@ async function saveSystemSettings() { // 🌟 新增:全域變數用來暫存所有快照資料,實現前端秒速過濾 let allBackupsData = []; -// 1. 建立新快照 +// 1. 建立新快照 - 🌟 升級為真實動態訊息流 (含百分比動畫) async function createSnapshot() { - // 🛡️ 防線一:必須先連線才能備份 if (!isConnected) { return alert("⚠️ 請先點擊畫面上方的「連線至 CMTS」成功後,再執行備份任務!\n(確保您清楚目前正在操作哪一台設備)"); } @@ -964,7 +1036,6 @@ async function createSnapshot() { const snapshotName = document.getElementById('snapshotName').value.trim(); if (!snapshotName) return alert("請輸入快照名稱!"); - // 🟢 抓取描述輸入框的值 const snapshotDescription = document.getElementById('snapshotDescription')?.value.trim() || ""; const configType = document.getElementById('backupConfigType').value; const btn = document.getElementById('btnCreateSnapshot'); @@ -972,6 +1043,10 @@ async function createSnapshot() { btn.disabled = true; msg.style.display = 'inline-block'; + msg.style.color = '#f39c12'; // 🌟 確保橘色 + msg.innerHTML = '⏳ 準備連線至設備...'; + + let progressInterval = null; // 🌟 新增:進度條計時器 try { const response = await fetch('/api/v1/backups/snapshot', { @@ -982,25 +1057,84 @@ async function createSnapshot() { username: connInfo.user, password: connInfo.pass, snapshot_name: snapshotName, - description: snapshotDescription, // 🟢 將描述傳給後端 + description: snapshotDescription, config_type: configType }) }); - const result = await response.json(); - - if (result.status === 'success') { - alert(`✅ ${result.message}`); - document.getElementById('snapshotName').value = ''; // 清空名稱輸入框 - if (document.getElementById('snapshotDescription')) { - document.getElementById('snapshotDescription').value = ''; // 🟢 清空描述輸入框 + + if (!response.ok) throw new Error(`伺服器回應錯誤: ${response.status}`); + + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let buffer = ""; + let isSuccess = false; + let finalMessage = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + let lines = buffer.split('\n'); + buffer = lines.pop(); + + for (let line of lines) { + if (!line.trim()) continue; + try { + const data = JSON.parse(line); + if (data.status === 'progress') { + // 🌟 導入百分比動畫邏輯 + if (data.message.includes('正在下載')) { + let progress = 0; + if (progressInterval) clearInterval(progressInterval); + + progressInterval = setInterval(() => { + progress += (95 - progress) * 0.06; + if (msg) { + msg.innerHTML = `📥 正在下載 ${configType} 配置檔 (${Math.round(progress)}%)...`; + } + }, 500); + } else { + if (progressInterval) { + clearInterval(progressInterval); + progressInterval = null; + if (msg) { + msg.innerHTML = `📥 正在下載 ${configType} 配置檔 (100%) - 下載完成!`; + msg.style.color = "#27ae60"; // 瞬間變綠色 + } + await new Promise(resolve => setTimeout(resolve, 600)); + if (msg) msg.style.color = "#f39c12"; // 恢復橘色 + } + if (msg) { + msg.style.color = '#f39c12'; + msg.innerHTML = data.message; + } + } + } else if (data.status === 'success') { + isSuccess = true; + finalMessage = data.message; + } else if (data.status === 'error') { + throw new Error(data.message); + } + } catch (e) { + if (e.message) throw e; + console.warn("解析串流 JSON 失敗:", line); + } } - loadBackupHistory(); // 重新載入歷史紀錄 - } else { - alert(`❌ 備份失敗: ${result.message}`); + } + + if (isSuccess) { + alert(`✅ ${finalMessage}`); + document.getElementById('snapshotName').value = ''; + if (document.getElementById('snapshotDescription')) { + document.getElementById('snapshotDescription').value = ''; + } + loadBackupHistory(); } } catch (error) { alert(`❌ 發生錯誤: ${error.message}`); } finally { + if (progressInterval) clearInterval(progressInterval); // 🌟 確保清除計時器 btn.disabled = false; msg.style.display = 'none'; } @@ -1015,7 +1149,8 @@ async function loadBackupHistory() { const tbody = document.getElementById('backup-history-tbody'); if (!tbody) return; - tbody.innerHTML = '⏳ 正在載入歷史紀錄...'; + // 🌟 修正:將原本的灰色改為橘色並加粗 + tbody.innerHTML = '⏳ 正在載入歷史紀錄...'; try { // 🌟 關鍵修改:將 config_type 改為 'all',一次把該設備的所有快照撈回來 @@ -1174,8 +1309,9 @@ async function restoreBackup(snapshotId, snapshotName, backupHost) { // 1. 移除進度條,改為純文字百分比動態顯示 modalTargetInfo.innerText = `正在分析快照差異: ${snapshotName}`; + // 🌟 修正:將原本的藍色改為橘色 modalOutput.innerHTML = ` -
🔍 正在抓取設備當前配置,並與快照進行深度比對... (0%)
+
🔍 正在抓取設備當前配置,並與快照進行深度比對... (0%)
`; modal.classList.add('active'); diff --git a/static/edit-mode.js b/static/edit-mode.js index 683505b..5c8b048 100644 --- a/static/edit-mode.js +++ b/static/edit-mode.js @@ -314,7 +314,8 @@ export async function startEditLeaf(path, elementId) { const origVal = container.getAttribute('data-original'); const safeOrigVal = escapeHTML(origVal); - container.innerHTML = `⏳ 設備連線與載入選項中...`; + // 🌟 修正:統一為標準橘色並加粗 (原為 #e67e22) + container.innerHTML = `⏳ 設備連線與載入選項中...`; try { let optData = await apiGetLeafOptions(host, currentMode); @@ -746,7 +747,7 @@ async function applyEditLeaf(path, elementId) { export function executeSideCLI() { const finalScript = document.getElementById('side-cli-textarea').value; document.getElementById('side-pane-title').innerHTML = '⏳ 正在寫入設備...'; - document.getElementById('side-pane-title').style.color = '#3498db'; + document.getElementById('side-pane-title').style.color = '#f39c12'; // 🌟 統一載入中狀態為橘色 (原為 #3498db) document.getElementById('btn-side-cancel').style.display = 'none'; document.getElementById('btn-side-confirm').style.display = 'none'; document.getElementById('btn-side-close').style.display = 'none';