diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..58e2313
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "editor.inlineSuggest.enabled": false,
+ "json.schemaDownload.enable": false
+}
\ No newline at end of file
diff --git a/database.py b/database.py
index 04c5b2c..5ef7141 100644
--- a/database.py
+++ b/database.py
@@ -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"],
diff --git a/index.html b/index.html
index f20b803..af172ca 100644
--- a/index.html
+++ b/index.html
@@ -36,6 +36,8 @@
+
+
@@ -373,6 +375,50 @@
+
+
+
+
+
+
+
📸 建立新快照
+
+
+
+
+ ⏳ 正在連線設備並抓取設定,請稍候...
+
+
+
+
+
+
+
🗂️ 歷史備份紀錄
+
+
+
+
+
+
+ | 時間 |
+ 快照名稱 |
+ 類型 |
+ 操作 |
+
+
+
+
+ | 請點擊「重新整理」載入歷史紀錄... |
+
+
+
+
+
+
+
+
diff --git a/routers/backup.py b/routers/backup.py
index ce4e138..e43e346 100644
--- a/routers/backup.py
+++ b/routers/backup.py
@@ -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:
diff --git a/static/app.js b/static/app.js
index 1ad1639..a1e9124 100644
--- a/static/app.js
+++ b/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 = '
| ⏳ 正在載入歷史紀錄... |
';
+
+ 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 => `
+
+ | ${new Date(item.timestamp).toLocaleString()} |
+ ${item.snapshot_name} |
+ ${item.config_type} |
+
+
+
+ |
+
+ `).join('');
+ } else {
+ tbody.innerHTML = '| 尚無任何備份紀錄。 |
';
+ }
+ } catch (error) {
+ tbody.innerHTML = `| ❌ 載入失敗: ${error.message} |
`;
+ }
+}
+
+// 3. 檢視特定快照內容
+async function viewBackup(backupId) {
+ openModal("載入快照中...");
+ const outputEl = document.getElementById('modalOutput');
+ outputEl.innerHTML = `⏳ 正在向資料庫撈取快照資料,請稍候...`;
+
+ 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;
\ No newline at end of file