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" . }}
+
+
+
+ Backup
+
+ {{ 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
+
+
+
+
+ [[ formatFileSize(text) ]]
+
+
+ [[ formatBackupTime(text) ]]
+
+
+
+ Download
+
+ Restore
+
+
+ Delete
+
+
+
+
+
+
+
+