refactor: 混合式架構 (Hybrid Approach) 的設定載入訊息設計。

-前端模擬動態流 (Simulated Streaming)
   -真實動態流 (True Streaming)
This commit is contained in:
swpa 2026-06-03 17:24:08 +08:00
parent ecedee49d2
commit 3cd7ce253e
4 changed files with 456 additions and 150 deletions

View File

@ -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 = `<span style="color: #f39c12;">正在向 ${connInfo.host} 查詢中,請稍候...</span>`;
// 🌟 模擬動態訊息流邏輯
const states = [
`🔌 正在建立 SSH 連線至 ${connInfo.host}...`,
`📥 正在向設備發送查詢指令...`,
`🧩 正在等待設備回傳資料...`
];
let progressIdx = 0;
outputEl.innerHTML = `<span id="query-progress-text" style="color: #f39c12; font-weight: bold;">${states[0]}</span>`;
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 = `<div style="color: #c0392b; font-weight: bold;">❌ 錯誤: ${result.message}</div>`;
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 = `<div style="color: #c0392b; font-weight: bold;">❌ 連線失敗: ${error.message}</div>`;
container.innerHTML = `<div style="color: #c0392b; font-weight: bold;">❌ 連線或解析失敗: ${error.message}</div>`;
} 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)}%)...`;
}
loadBackupHistory(); // 重新載入歷史紀錄
}, 500);
} else {
alert(`❌ 備份失敗: ${result.message}`);
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);
}
}
}
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 = '<tr><td colspan="5" style="padding: 20px; text-align: center; color: #7f8c8d;">⏳ 正在載入歷史紀錄...</td></tr>';
// 🌟 修正:將原本的灰色改為橘色並加粗
tbody.innerHTML = '<tr><td colspan="5" style="padding: 20px; text-align: center; color: #f39c12; font-weight: bold;">⏳ 正在載入歷史紀錄...</td></tr>';
try {
// 🌟 關鍵修改:將 config_type 改為 'all',一次把該設備的所有快照撈回來
@ -3203,8 +3338,9 @@ async function restoreBackup(snapshotId, snapshotName, backupHost) {
// 1. 移除進度條,改為純文字百分比動態顯示
modalTargetInfo.innerText = `正在分析快照差異: ${snapshotName}`;
// 🌟 修正:將原本的藍色改為橘色
modalOutput.innerHTML = `
<div id="diff-progress-text" style="color: #3498db; margin-bottom: 10px; font-weight: bold; font-size: 15px;">🔍 正在抓取設備當前配置,並與快照進行深度比對... (0%)</div>
<div id="diff-progress-text" style="color: #f39c12; margin-bottom: 10px; font-weight: bold; font-size: 15px;">🔍 正在抓取設備當前配置,並與快照進行深度比對... (0%)</div>
`;
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 = `<span style="font-size: 12px; color: #e67e22; margin-left: 5px;">⏳ 設備連線與載入選項中...</span>`;
// 🌟 修正:統一為標準橘色並加粗 (原為 #e67e22)
container.innerHTML = `<span style="font-size: 12px; color: #f39c12; margin-left: 5px; font-weight: bold;">⏳ 設備連線與載入選項中...</span>`;
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):
async def backup_streamer():
try:
raw_cli = await fetch_raw_config(req.host, req.username, req.password, req.config_type)
parsed_tree = parse_cli_to_tree(raw_cli)
# 1. 廣播:正在連線
yield json.dumps({"status": "progress", "message": f"🔌 正在建立 SSH 連線至 {req.host}..."}) + "\n"
await asyncio.sleep(0.1)
# 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)
# 3. 廣播:正在解析
yield json.dumps({"status": "progress", "message": "🧩 正在解析配置並建構樹狀圖結構..."}) + "\n"
parsed_tree = await asyncio.to_thread(parse_cli_to_tree, raw_cli)
# 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, # 🟢 將描述傳遞給資料庫函數
description=req.description,
is_auto=False
)
if not backup_id:
return {"status": "error", "message": "資料庫寫入失敗,請檢查系統日誌"}
yield json.dumps({"status": "error", "message": "資料庫寫入失敗,請檢查系統日誌"}) + "\n"
return
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}")
return {"status": "error", "message": f"設備連線或解析失敗: {str(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):

View File

@ -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):
async def backup_streamer():
try:
raw_cli = await fetch_raw_config(req.host, req.username, req.password, req.config_type)
parsed_tree = parse_cli_to_tree(raw_cli)
# 1. 廣播:正在連線
yield json.dumps({"status": "progress", "message": f"🔌 正在建立 SSH 連線至 {req.host}..."}) + "\n"
await asyncio.sleep(0.1)
# 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)
# 3. 廣播:正在解析
yield json.dumps({"status": "progress", "message": "🧩 正在解析配置並建構樹狀圖結構..."}) + "\n"
parsed_tree = await asyncio.to_thread(parse_cli_to_tree, raw_cli)
# 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, # 🟢 將描述傳遞給資料庫函數
description=req.description,
is_auto=False
)
if not backup_id:
return {"status": "error", "message": "資料庫寫入失敗,請檢查系統日誌"}
yield json.dumps({"status": "error", "message": "資料庫寫入失敗,請檢查系統日誌"}) + "\n"
return
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}")
return {"status": "error", "message": f"設備連線或解析失敗: {str(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):

View File

@ -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 = `<span style="color: #f39c12;">正在向 ${connInfo.host} 查詢中,請稍候...</span>`;
// 🌟 模擬動態訊息流邏輯
const states = [
`🔌 正在建立 SSH 連線至 ${connInfo.host}...`,
`📥 正在向設備發送查詢指令...`,
`🧩 正在等待設備回傳資料...`
];
let progressIdx = 0;
outputEl.innerHTML = `<span id="query-progress-text" style="color: #f39c12; font-weight: bold;">${states[0]}</span>`;
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 = `<div style="color: #c0392b; font-weight: bold;">❌ 錯誤: ${result.message}</div>`;
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 = `<div style="color: #c0392b; font-weight: bold;">❌ 連線失敗: ${error.message}</div>`;
container.innerHTML = `<div style="color: #c0392b; font-weight: bold;">❌ 連線或解析失敗: ${error.message}</div>`;
} 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)}%)...`;
}
loadBackupHistory(); // 重新載入歷史紀錄
}, 500);
} else {
alert(`❌ 備份失敗: ${result.message}`);
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);
}
}
}
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 = '<tr><td colspan="5" style="padding: 20px; text-align: center; color: #7f8c8d;">⏳ 正在載入歷史紀錄...</td></tr>';
// 🌟 修正:將原本的灰色改為橘色並加粗
tbody.innerHTML = '<tr><td colspan="5" style="padding: 20px; text-align: center; color: #f39c12; font-weight: bold;">⏳ 正在載入歷史紀錄...</td></tr>';
try {
// 🌟 關鍵修改:將 config_type 改為 'all',一次把該設備的所有快照撈回來
@ -1174,8 +1309,9 @@ async function restoreBackup(snapshotId, snapshotName, backupHost) {
// 1. 移除進度條,改為純文字百分比動態顯示
modalTargetInfo.innerText = `正在分析快照差異: ${snapshotName}`;
// 🌟 修正:將原本的藍色改為橘色
modalOutput.innerHTML = `
<div id="diff-progress-text" style="color: #3498db; margin-bottom: 10px; font-weight: bold; font-size: 15px;">🔍 正在抓取設備當前配置並與快照進行深度比對... (0%)</div>
<div id="diff-progress-text" style="color: #f39c12; margin-bottom: 10px; font-weight: bold; font-size: 15px;">🔍 正在抓取設備當前配置並與快照進行深度比對... (0%)</div>
`;
modal.classList.add('active');

View File

@ -314,7 +314,8 @@ export async function startEditLeaf(path, elementId) {
const origVal = container.getAttribute('data-original');
const safeOrigVal = escapeHTML(origVal);
container.innerHTML = `<span style="font-size: 12px; color: #e67e22; margin-left: 5px;">⏳ 設備連線與載入選項中...</span>`;
// 🌟 修正:統一為標準橘色並加粗 (原為 #e67e22)
container.innerHTML = `<span style="font-size: 12px; color: #f39c12; margin-left: 5px; font-weight: bold;">⏳ 設備連線與載入選項中...</span>`;
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';