feat: Core Backup Engine & Async Stability
This commit is contained in:
parent
9de6896363
commit
cfc4060d2b
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"editor.inlineSuggest.enabled": false,
|
||||
"json.schemaDownload.enable": false
|
||||
}
|
||||
|
|
@ -328,7 +328,7 @@ async def get_config_backup_detail(backup_id: str) -> Optional[Dict[str, Any]]:
|
|||
return None
|
||||
|
||||
query = """
|
||||
SELECT id, timestamp, snapshot_name, is_auto, parsed_tree
|
||||
SELECT id, host, timestamp, snapshot_name, is_auto, parsed_tree
|
||||
FROM config_backups
|
||||
WHERE id = $1;
|
||||
"""
|
||||
|
|
@ -341,6 +341,7 @@ async def get_config_backup_detail(backup_id: str) -> Optional[Dict[str, Any]]:
|
|||
|
||||
return {
|
||||
"id": str(record["id"]),
|
||||
"host": record["host"],
|
||||
"timestamp": record["timestamp"].isoformat(),
|
||||
"snapshot_name": record["snapshot_name"],
|
||||
"is_auto": record["is_auto"],
|
||||
|
|
|
|||
46
index.html
46
index.html
|
|
@ -36,6 +36,8 @@
|
|||
<button class="tab-btn" onclick="switchTab('config-tab', this)">⚙️ CMTS 設備配置</button>
|
||||
<!-- 💡 新增:系統設定頁籤 -->
|
||||
<button class="tab-btn" onclick="switchTab('settings-tab', this); openSystemSettings();">⚙️ 系統設定</button>
|
||||
<!-- 💡 新增:備份與還原頁籤 -->
|
||||
<button class="tab-btn" onclick="switchTab('backup-tab', this); loadBackupHistory();">💾 設備備份與還原</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab 1: True SSH Terminal -->
|
||||
|
|
@ -373,6 +375,50 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 💡 Tab 5: 設備備份與還原 -->
|
||||
<div id="backup-tab" class="tab-content" style="overflow-y: auto; background-color: #f5f7fa;">
|
||||
<div class="manager-container">
|
||||
|
||||
<!-- 上半部:建立新快照 -->
|
||||
<div class="control-group" style="background: #ffffff; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.02); border: 1px solid #e2e8f0;">
|
||||
<h3 style="margin-top: 0; font-size: 18px; color: #2980b9; border-bottom: 2px solid #ecf0f1; padding-bottom: 10px; margin-bottom: 15px;">📸 建立新快照</h3>
|
||||
<div style="display: flex; align-items: center; gap: 15px;">
|
||||
<input type="text" id="snapshotName" placeholder="請輸入快照名稱 (例如: 例行備份)" style="width: 300px; padding: 8px; border: 1px solid #bdc3c7; border-radius: 4px;">
|
||||
<select id="backupConfigType" style="padding: 8px; border: 1px solid #bdc3c7; border-radius: 4px;">
|
||||
<option value="running">Running Config</option>
|
||||
</select>
|
||||
<button onclick="createSnapshot()" id="btnCreateSnapshot" class="btn-modern btn-save">🚀 立即備份</button>
|
||||
<span id="backup-status-msg" style="color: #e67e22; font-size: 14px; display: none;">⏳ 正在連線設備並抓取設定,請稍候...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下半部:歷史紀錄列表 -->
|
||||
<div class="control-group" style="background: #ffffff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.02); border: 1px solid #e2e8f0;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #ecf0f1; padding-bottom: 10px; margin-bottom: 15px;">
|
||||
<h3 style="margin: 0; font-size: 18px; color: #2c3e50;">🗂️ 歷史備份紀錄</h3>
|
||||
<button onclick="loadBackupHistory()" class="btn-modern btn-scan" style="padding: 5px 10px; font-size: 13px;">🔄 重新整理</button>
|
||||
</div>
|
||||
|
||||
<table style="width: 100%; border-collapse: collapse; text-align: left;">
|
||||
<thead>
|
||||
<tr style="background-color: #f8f9fa; border-bottom: 2px solid #bdc3c7;">
|
||||
<th style="padding: 10px; color: #34495e;">時間</th>
|
||||
<th style="padding: 10px; color: #34495e;">快照名稱</th>
|
||||
<th style="padding: 10px; color: #34495e;">類型</th>
|
||||
<th style="padding: 10px; color: #34495e; text-align: right;">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="backup-history-tbody">
|
||||
<tr>
|
||||
<td colspan="4" style="padding: 20px; text-align: center; color: #7f8c8d;">請點擊「重新整理」載入歷史紀錄...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 獨立的彈出式輸出視窗 (Modal) -->
|
||||
<div id="outputModal" class="modal-overlay" onclick="closeModal(event)">
|
||||
<div class="modal-container" onclick="event.stopPropagation()">
|
||||
|
|
|
|||
|
|
@ -72,7 +72,8 @@ async def create_snapshot(req: SnapshotRequest):
|
|||
return {
|
||||
"status": "success",
|
||||
"message": f"快照 '{req.snapshot_name}' 建立成功!",
|
||||
"backup_id": backup_id
|
||||
"backup_id": backup_id,
|
||||
"host": req.host
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
140
static/app.js
140
static/app.js
|
|
@ -795,6 +795,134 @@ async function saveSystemSettings() {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 🌟 新增:設備備份與還原 (Backup & Restore)
|
||||
// ============================================================================
|
||||
|
||||
// 1. 建立新快照
|
||||
async function createSnapshot() {
|
||||
const connInfo = getGlobalConnectionInfo();
|
||||
if (!connInfo || !connInfo.host) return alert("請先在畫面上方輸入設備連線資訊!");
|
||||
|
||||
const snapshotName = document.getElementById('snapshotName').value.trim();
|
||||
if (!snapshotName) return alert("請輸入快照名稱!");
|
||||
|
||||
const configType = document.getElementById('backupConfigType').value;
|
||||
const btn = document.getElementById('btnCreateSnapshot');
|
||||
const msg = document.getElementById('backup-status-msg');
|
||||
|
||||
btn.disabled = true;
|
||||
msg.style.display = 'inline-block';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/backups/snapshot', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
host: connInfo.host,
|
||||
username: connInfo.user,
|
||||
password: connInfo.pass,
|
||||
snapshot_name: snapshotName,
|
||||
config_type: configType
|
||||
})
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'success') {
|
||||
alert(`✅ ${result.message}`);
|
||||
document.getElementById('snapshotName').value = ''; // 清空輸入框
|
||||
loadBackupHistory(); // 重新載入歷史紀錄
|
||||
} else {
|
||||
alert(`❌ 備份失敗: ${result.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`❌ 發生錯誤: ${error.message}`);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
msg.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 載入歷史紀錄列表
|
||||
async function loadBackupHistory() {
|
||||
const connInfo = getGlobalConnectionInfo();
|
||||
if (!connInfo || !connInfo.host) return;
|
||||
|
||||
const tbody = document.getElementById('backup-history-tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
tbody.innerHTML = '<tr><td colspan="4" style="padding: 20px; text-align: center; color: #7f8c8d;">⏳ 正在載入歷史紀錄...</td></tr>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/backups/?host=${encodeURIComponent(connInfo.host)}&config_type=running`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'success' && result.data.length > 0) {
|
||||
tbody.innerHTML = result.data.map(item => `
|
||||
<tr style="border-bottom: 1px solid #ecf0f1; transition: background-color 0.2s;" onmouseover="this.style.backgroundColor='#fcfcfc'" onmouseout="this.style.backgroundColor='transparent'">
|
||||
<td style="padding: 12px 10px; color: #7f8c8d; font-size: 14px;">${new Date(item.timestamp).toLocaleString()}</td>
|
||||
<td style="padding: 12px 10px; font-weight: bold; color: #2c3e50;">${item.snapshot_name}</td>
|
||||
<td style="padding: 12px 10px; color: #34495e;"><span style="background: #e8f8f5; color: #27ae60; padding: 3px 8px; border-radius: 12px; font-size: 12px;">${item.config_type}</span></td>
|
||||
<td style="padding: 12px 10px; text-align: right;">
|
||||
<button onclick="viewBackup('${item.id}')" class="btn-modern btn-scan" style="padding: 4px 8px; font-size: 12px; margin-right: 5px;">👁️ 檢視</button>
|
||||
<button onclick="deleteBackup('${item.id}')" class="btn-modern btn-disconnect" style="padding: 4px 8px; font-size: 12px;">🗑️ 刪除</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
tbody.innerHTML = '<tr><td colspan="4" style="padding: 20px; text-align: center; color: #7f8c8d;">尚無任何備份紀錄。</td></tr>';
|
||||
}
|
||||
} catch (error) {
|
||||
tbody.innerHTML = `<tr><td colspan="4" style="padding: 20px; text-align: center; color: #e74c3c;">❌ 載入失敗: ${error.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 檢視特定快照內容
|
||||
async function viewBackup(backupId) {
|
||||
openModal("載入快照中...");
|
||||
const outputEl = document.getElementById('modalOutput');
|
||||
outputEl.innerHTML = `<span style="color: #f39c12;">⏳ 正在向資料庫撈取快照資料,請稍候...</span>`;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/backups/${backupId}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'success') {
|
||||
const data = result.data;
|
||||
const infoHeader = `【快照名稱】: ${data.snapshot_name}\n【建立時間】: ${new Date(data.timestamp).toLocaleString()}\n【設備 IP】: ${data.host}\n==================================================\n\n`;
|
||||
|
||||
// 如果後端有回傳 raw_cli 就優先顯示,否則將 parsed_tree 轉為字串顯示
|
||||
const content = data.raw_cli ? data.raw_cli : JSON.stringify(data.parsed_tree, null, 2);
|
||||
|
||||
outputEl.style.color = "#e0e0e0";
|
||||
outputEl.textContent = infoHeader + content;
|
||||
} else {
|
||||
outputEl.style.color = "#e74c3c";
|
||||
outputEl.textContent = `❌ 讀取失敗: ${result.message}`;
|
||||
}
|
||||
} catch (error) {
|
||||
outputEl.style.color = "#e74c3c";
|
||||
outputEl.textContent = `❌ 連線失敗: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 刪除快照
|
||||
async function deleteBackup(backupId) {
|
||||
if (!confirm("⚠️ 確定要永久刪除這筆備份紀錄嗎?此動作無法復原!")) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/backups/${backupId}`, { method: 'DELETE' });
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'success') {
|
||||
loadBackupHistory(); // 刪除成功後重新整理列表
|
||||
} else {
|
||||
alert(`❌ 刪除失敗: ${result.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`❌ 發生錯誤: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 🌟 6. 共用 UI 元件與狀態管理 (Modals & Button States)
|
||||
|
|
@ -961,3 +1089,15 @@ window.executeSideCLI = executeSideCLI;
|
|||
window.toggleChildCheckboxes = toggleChildCheckboxes;
|
||||
window.updateParentCheckboxState = updateParentCheckboxState;
|
||||
window.closeModal = closeModal;
|
||||
|
||||
// --- 備份與還原 ---
|
||||
window.createSnapshot = createSnapshot;
|
||||
window.loadBackupHistory = loadBackupHistory;
|
||||
window.viewBackup = viewBackup;
|
||||
window.deleteBackup = deleteBackup;
|
||||
|
||||
// 將備份還原相關函數綁定到全域,供 HTML onclick 使用
|
||||
window.loadBackupHistory = loadBackupHistory;
|
||||
window.createSnapshot = createSnapshot;
|
||||
//window.restoreSnapshot = restoreSnapshot;
|
||||
//window.deleteSnapshot = deleteSnapshot;
|
||||
Loading…
Reference in New Issue