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';