From 8a861894c97cfa9d35d3e7b83f9c264c0afb4110 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 26 Apr 2026 19:46:16 +0800 Subject: [PATCH] feat: add backup UI page and frontend model fields --- .../2026-04-26-backup-ui-page-and-model.md | 37 +++++++++ web/assets/js/model/setting.js | 5 ++ web/html/settings.html | 83 ++++++++++++++++++- web/html/settings/backup.html | 79 ++++++++++++++++++ 4 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 docs/Tasktracking/2026-04-26-backup-ui-page-and-model.md create mode 100644 web/html/settings/backup.html diff --git a/docs/Tasktracking/2026-04-26-backup-ui-page-and-model.md b/docs/Tasktracking/2026-04-26-backup-ui-page-and-model.md new file mode 100644 index 00000000..1d958490 --- /dev/null +++ b/docs/Tasktracking/2026-04-26-backup-ui-page-and-model.md @@ -0,0 +1,37 @@ +# Task Record + +Date: 2026-04-26 +Related Module: web (frontend) +Change Type: Add + +## Background +Add backup management UI page and corresponding frontend model fields to the settings page. The backup feature allows users to configure scheduled backups, manually create backups, and manage (download/restore/delete) existing backup files. + +## Changes +- Added 4 backup fields to `AllSetting` class in `web/assets/js/model/setting.js`: `backupEnabled`, `backupFrequency`, `backupHour`, `backupMaxCount` +- Created `web/html/settings/backup.html` with Vue template for backup configuration, manual operations, and backup list table +- Added Backup tab (key="7") to settings.html `` component +- Added Vue data properties: `backupList`, `backupColumns`, `backupLoading`, `backupCreating`, `backupRefreshInterval` +- Added Vue methods: `fetchBackups`, `createBackup`, `restoreBackup`, `deleteBackup`, `downloadBackup`, `formatFileSize`, `formatBackupTime` +- Modified `onSettingsTabChange` to fetch backups when tab 7 is selected +- Added 30-second backup refresh interval in `mounted()` lifecycle hook +- Added `beforeDestroy()` lifecycle hook to clear the backup refresh interval + +## Impact +- New template file: `web/html/settings/backup.html` +- Modified: `web/assets/js/model/setting.js`, `web/html/settings.html` +- Uses Vue `[[ ]]` delimiters for interpolation (project standard) +- API endpoints referenced: `/panel/api/server/listBackups`, `/panel/api/server/backup`, `/panel/api/server/restore/`, `/panel/api/server/deleteBackup/`, `/panel/api/server/downloadBackup/` +- Uses `this.authHeaders` for axios requests (expected to be defined separately) +- `AllSetting.equals()` will now factor in backup fields for change detection + +## Verification +- `go build ./...` passes +- `gofmt -l -w .` passes with no changes needed +- Template compilation verified via build +- Cannot runtime test without running panel server (no local dev environment) + +## Risks And Follow-Up +- `this.authHeaders` is referenced but not defined in the current codebase; must be defined before runtime use +- Backup API endpoints must exist on the backend for the page to function +- `AllSetting.equals()` will use the new backup fields for save-button change detection (existing behavior, no regression expected) diff --git a/web/assets/js/model/setting.js b/web/assets/js/model/setting.js index a3c15dca..bbf37c87 100644 --- a/web/assets/js/model/setting.js +++ b/web/assets/js/model/setting.js @@ -81,6 +81,11 @@ class AllSetting { this.ldapDefaultExpiryDays = 0; this.ldapDefaultLimitIP = 0; + this.backupEnabled = false; + this.backupFrequency = "daily"; + this.backupHour = 3; + this.backupMaxCount = 10; + this.dbType = "sqlite"; this.dbHost = "127.0.0.1"; this.dbPort = "3306"; diff --git a/web/html/settings.html b/web/html/settings.html index 938418ae..9e0bad6f 100644 --- a/web/html/settings.html +++ b/web/html/settings.html @@ -107,6 +107,13 @@ {{ template "settings/panel/subscription/clash" . }} + + + {{ template "settings/backup" . }} + @@ -251,10 +258,21 @@ const sample = []; this.remarkModel.forEach(r => sample.push(this.remarkModels[r])); this.remarkSample = sample.length == 0 ? '' : sample.join(this.remarkSeparator); - } + }, + backupList: [], + backupColumns: [ + { title: 'Filename', dataIndex: 'filename', key: 'filename' }, + { title: 'Timestamp', dataIndex: 'timestamp', key: 'timestamp', scopedSlots: { customRender: 'timestamp' } }, + { title: 'Size', dataIndex: 'size', key: 'size', scopedSlots: { customRender: 'size' } }, + { title: 'Actions', key: 'action', scopedSlots: { customRender: 'action' } } + ], + backupLoading: false, + backupCreating: false, + backupRefreshInterval: null, }, methods: { onSettingsTabChange(key) { + if (key === '7') { this.fetchBackups(); } }, loading(spinning = true) { this.loadingStates.spinning = spinning; @@ -451,6 +469,65 @@ updatedNoises[index] = { ...updatedNoises[index], applyTo: value }; this.noisesArray = updatedNoises; }, + fetchBackups() { + this.backupLoading = true; + axios.get(this.entryHost + 'panel/api/server/listBackups', { + headers: this.authHeaders + }).then(res => { + this.backupList = res.data.obj || []; + }).catch(err => { + this.$message.error('Failed to load backups'); + }).finally(() => { + this.backupLoading = false; + }); + }, + createBackup() { + this.backupCreating = true; + axios.post(this.entryHost + 'panel/api/server/backup', {}, { + headers: this.authHeaders + }).then(res => { + this.$message.success('Backup created successfully'); + this.fetchBackups(); + }).catch(err => { + this.$message.error('Backup failed'); + }).finally(() => { + this.backupCreating = false; + }); + }, + restoreBackup(filename) { + axios.post(this.entryHost + 'panel/api/server/restore/' + filename, {}, { + headers: this.authHeaders + }).then(res => { + this.$message.success('Restore completed'); + this.fetchBackups(); + }).catch(err => { + this.$message.error('Restore failed'); + }); + }, + deleteBackup(filename) { + axios.post(this.entryHost + 'panel/api/server/deleteBackup/' + filename, {}, { + headers: this.authHeaders + }).then(res => { + this.$message.success('Backup deleted'); + this.fetchBackups(); + }).catch(err => { + this.$message.error('Delete failed'); + }); + }, + downloadBackup(filename) { + window.open(this.entryHost + 'panel/api/server/downloadBackup/' + filename, '_blank'); + }, + formatFileSize(bytes) { + if (!bytes || bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; + }, + formatBackupTime(ts) { + if (!ts) return ''; + return ts.replace(/(\d{4})-(\d{2})-(\d{2})-(\d{2})(\d{2})(\d{2})/, '$1-$2-$3 $4:$5:$6'); + }, }, computed: { ldapInboundTagList: { @@ -655,6 +732,7 @@ } finally { this.loadingStates.fetched = true; } + this.backupRefreshInterval = setInterval(() => { this.fetchBackups(); }, 30000); if (!settingsLoaded) { return; } @@ -666,6 +744,9 @@ console.error('Settings change detection error:', e); } } + }, + beforeDestroy() { + if (this.backupRefreshInterval) { clearInterval(this.backupRefreshInterval); } } }); diff --git a/web/html/settings/backup.html b/web/html/settings/backup.html new file mode 100644 index 00000000..48a30744 --- /dev/null +++ b/web/html/settings/backup.html @@ -0,0 +1,79 @@ +
+ + + + + + + + + + + + + + Every Hour + Every 12 Hours + Every Day + Every Week + + + + + + + + + + + + + + + + + + + + + + Create Backup Now + + + + + + + Backup List + + + + + + + + + + +