Compare commits

..
Author SHA1 Message Date
Awuqing ed13194de1 feat(admin): 优化访问管理布局与操作流程 2026-08-11 11:37:39 +08:00
19 changed files with 45 additions and 119 deletions
+4 -4
View File
@@ -28,12 +28,12 @@
<table>
<tr>
<td width="50%"><img src="screenshots/dashboard.png" alt="BackupX dashboard with 30-day backup trends, storage distribution, and task health"><br><sub><strong>Dashboard</strong> — 30-day success and failure trends, storage distribution, task volume, and cluster health.</sub></td>
<td width="50%"><img src="screenshots/backup-tasks.png" alt="BackupX task list with schedules, targets, retention, tags, RPO, and verification status"><br><sub><strong>Backup tasks</strong> — schedules, multi-target policies, retention, tags, RPO goals, and recurring verification.</sub></td>
<td width="50%"><img src="screenshots/dashboard.png" alt="Dashboard"></td>
<td width="50%"><img src="screenshots/backup-tasks.png" alt="Backup Tasks"></td>
</tr>
<tr>
<td><img src="screenshots/storage-targets.png" alt="BackupX storage targets with connection health, capacity usage, and redundancy roles"><br><sub><strong>Storage targets</strong> — connection health, live capacity, favourites, and redundancy roles at a glance.</sub></td>
<td><img src="screenshots/backup-records.png" alt="BackupX backup history with success and failure states, checksums, destinations, and retention locks"><br><sub><strong>Backup records</strong> — success and failure states, checksums, destinations, execution logs, and retention locks.</sub></td>
<td><img src="screenshots/storage-targets.png" alt="Storage Targets"></td>
<td><img src="screenshots/backup-records.png" alt="Backup Records"></td>
</tr>
</table>
+4 -4
View File
@@ -28,12 +28,12 @@
<table>
<tr>
<td width="50%"><img src="screenshots/dashboard.png" alt="BackupX 仪表盘,展示 30 天备份趋势、存储分布和任务健康度"><br><sub><strong>仪表盘</strong> — 30 天成功/失败趋势、存储分布、任务规模与集群健康度。</sub></td>
<td width="50%"><img src="screenshots/backup-tasks.png" alt="BackupX 备份任务列表,展示调度、存储目标、保留策略、标签、RPO 和验证状态"><br><sub><strong>备份任务</strong> — 调度、双目标策略、保留规则、标签、RPO 与定时验证。</sub></td>
<td width="50%"><img src="screenshots/dashboard.png" alt="仪表盘"></td>
<td width="50%"><img src="screenshots/backup-tasks.png" alt="备份任务"></td>
</tr>
<tr>
<td><img src="screenshots/storage-targets.png" alt="BackupX 存储目标,展示连接状态、容量使用和冗余角色"><br><sub><strong>存储目标</strong> — 连接状态、实时容量、收藏及冗余角色一目了然。</sub></td>
<td><img src="screenshots/backup-records.png" alt="BackupX 备份历史,展示成功失败状态、校验和、存储位置及保留锁"><br><sub><strong>备份记录</strong> — 成功/失败状态、校验和、落盘位置、执行日志与保留锁定。</sub></td>
<td><img src="screenshots/storage-targets.png" alt="存储目标"></td>
<td><img src="screenshots/backup-records.png" alt="备份记录"></td>
</tr>
</table>
+12 -12
View File
@@ -47,18 +47,18 @@
"showcase.tab.tasks": {"message": "备份任务"},
"showcase.tab.storage": {"message": "存储目标"},
"showcase.tab.nodes": {"message": "多节点"},
"showcase.dashboard.alt": {"message": "BackupX 仪表盘,展示 30 天备份趋势、存储分布、任务规模和集群健康度"},
"showcase.dashboard.title": {"message": "一眼掌握运行态"},
"showcase.dashboard.desc": {"message": "在一个实时运维视图中掌握 30 天成功/失败趋势、存储分布、任务数量、数据量和最近执行记录。"},
"showcase.tasks.alt": {"message": "BackupX 任务列表,展示调度、存储目标、保留策略、标签、RPO 目标和验证状态"},
"showcase.tasks.title": {"message": "策略状态清晰可扫"},
"showcase.tasks.desc": {"message": "集中查看调度、多目标策略、保留规则、标签、RPO 目标和验证状态,并可一键操作任意任务。"},
"showcase.storage.alt": {"message": "BackupX 存储目标,展示连接状态、实时容量、收藏和冗余角色"},
"showcase.storage.title": {"message": "每个目标,统一视图"},
"showcase.storage.desc": {"message": "统一比较本地磁盘与 70+ 远程后端的连接状态、实时容量、收藏及冗余角色。"},
"showcase.nodes.alt": {"message": "BackupX 节点列表,展示健康度、Agent 版本、队列深度、标签和心跳时间"},
"showcase.nodes.title": {"message": "集群健康一屏掌握"},
"showcase.nodes.desc": {"message": "集中监控本地 Master 与每个远程节点的健康度、Agent 版本、队列深度、标签和心跳时间。"},
"showcase.dashboard.alt": {"message": "显示备份健康和存储使用情况的 BackupX 仪表盘"},
"showcase.dashboard.title": {"message": "一眼掌握全局"},
"showcase.dashboard.desc": {"message": "备份成功率、存储使用量、最近执行记录和即将触发的计划集中显示在一个实时页面。"},
"showcase.tasks.alt": {"message": "BackupX 备份任务管理界面"},
"showcase.tasks.title": {"message": "可视化任务编辑器"},
"showcase.tasks.desc": {"message": "通过三步向导配置文件、MySQL、PostgreSQL、SQLite 和 SAP HANA,并绑定调度、多目标、保留、压缩和加密。"},
"showcase.storage.alt": {"message": "BackupX 存储目标管理界面"},
"showcase.storage.title": {"message": "多种后端,统一流程"},
"showcase.storage.desc": {"message": "以一致表单管理阿里云 OSS、腾讯云 COS、S3、Google Drive、WebDAV 及 rclone 后端,并测试连接和查看容量。"},
"showcase.nodes.alt": {"message": "BackupX 远程节点管理界面"},
"showcase.nodes.title": {"message": "快速搭建 Master-Agent"},
"showcase.nodes.desc": {"message": "创建节点、复制令牌并启动远程 Agent。任务在节点本地执行并直接上传存储,无需反向网络连通。"},
"showcase.cta": {"message": "开始阅读文档"},
"community.tag": {"message": "社区"},
@@ -25,14 +25,11 @@ function useTabs(): Tab[] {
label: <Translate id="showcase.tab.dashboard">Dashboard</Translate>,
icon: 'monitor',
image: useBaseUrl('/img/screenshots/dashboard.png'),
imageAlt: translate({
id: 'showcase.dashboard.alt',
message: 'BackupX dashboard showing 30-day backup trends, storage distribution, task volume and cluster health',
}),
title: <Translate id="showcase.dashboard.title">Operations at a glance</Translate>,
imageAlt: translate({id: 'showcase.dashboard.alt', message: 'BackupX dashboard showing backup health and storage usage'}),
title: <Translate id="showcase.dashboard.title">Know at a glance</Translate>,
description: (
<Translate id="showcase.dashboard.desc">
Track 30-day success and failure trends, storage distribution, task counts, data volume and recent runs from one live operations view.
Backup success rates, storage usage, recent runs and upcoming schedules all on one page with live data.
</Translate>
),
},
@@ -41,14 +38,11 @@ function useTabs(): Tab[] {
label: <Translate id="showcase.tab.tasks">Backup Tasks</Translate>,
icon: 'database',
image: useBaseUrl('/img/screenshots/backup-tasks.png'),
imageAlt: translate({
id: 'showcase.tasks.alt',
message: 'BackupX task list showing schedules, storage targets, retention, tags, RPO goals and verification status',
}),
title: <Translate id="showcase.tasks.title">Policies you can scan</Translate>,
imageAlt: translate({id: 'showcase.tasks.alt', message: 'BackupX backup task management screen'}),
title: <Translate id="showcase.tasks.title">Visual task editor</Translate>,
description: (
<Translate id="showcase.tasks.desc">
Review schedules, multi-target policies, retention, tags, RPO goals and verification status together, then act on any task in one click.
Files, MySQL, PostgreSQL, SQLite and SAP HANA with a three-step wizard. Cron editor, multi-target dispatch, retention, compression and encryption point and click.
</Translate>
),
},
@@ -57,14 +51,11 @@ function useTabs(): Tab[] {
label: <Translate id="showcase.tab.storage">Storage Targets</Translate>,
icon: 'storage',
image: useBaseUrl('/img/screenshots/storage-targets.png'),
imageAlt: translate({
id: 'showcase.storage.alt',
message: 'BackupX storage targets showing connection health, live capacity, favourites and redundancy roles',
}),
title: <Translate id="showcase.storage.title">Every target, one view</Translate>,
imageAlt: translate({id: 'showcase.storage.alt', message: 'BackupX storage target management screen'}),
title: <Translate id="showcase.storage.title">70+ backends, one flow</Translate>,
description: (
<Translate id="showcase.storage.desc">
Compare connection health, live capacity, favourites and redundancy roles across local disks and 70+ remote backends.
Alibaba OSS, Tencent COS, S3, Google Drive, WebDAV plus every rclone backend behind a uniform form. Test connection, favourite, and view live usage.
</Translate>
),
},
@@ -73,14 +64,11 @@ function useTabs(): Tab[] {
label: <Translate id="showcase.tab.nodes">Multi-Node</Translate>,
icon: 'network',
image: useBaseUrl('/img/screenshots/nodes.png'),
imageAlt: translate({
id: 'showcase.nodes.alt',
message: 'BackupX node list showing health, Agent versions, queue depth, labels and heartbeat times',
}),
title: <Translate id="showcase.nodes.title">Cluster health in one view</Translate>,
imageAlt: translate({id: 'showcase.nodes.alt', message: 'BackupX remote node management screen'}),
title: <Translate id="showcase.nodes.title">Master-Agent in minutes</Translate>,
description: (
<Translate id="showcase.nodes.desc">
Monitor health, Agent versions, queue depth, labels and heartbeat time across the local Master and every remote node.
Create a node, copy the token, start the Agent on any remote host. Tasks routed to a node run locally there and upload directly to storage no reverse connectivity required.
</Translate>
),
},
Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 96 KiB

+1 -1
View File
@@ -12,7 +12,6 @@ require (
github.com/prometheus/client_golang v1.23.2
github.com/rclone/rclone v1.73.5
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.10
github.com/spf13/viper v1.20.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.50.0
@@ -196,6 +195,7 @@ require (
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/samber/lo v1.52.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.10 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/sony/gobreaker v1.0.0 // indirect
@@ -59,7 +59,7 @@ func NewBackupRecordRepository(db *gorm.DB) *GormBackupRecordRepository {
func (r *GormBackupRecordRepository) List(ctx context.Context, options BackupRecordListOptions) ([]model.BackupRecord, error) {
// Omit("Manifest"):列表不需要可能很大的清单 JSON,避免每行拖出该 TEXT 列。
query := r.db.WithContext(ctx).Model(&model.BackupRecord{}).Omit("Manifest").Preload("Task").Preload("StorageTarget").Preload("Task.StorageTarget").Order("started_at desc")
query := r.db.WithContext(ctx).Model(&model.BackupRecord{}).Omit("Manifest").Preload("Task").Preload("Task.StorageTarget").Order("started_at desc")
if options.TaskID != nil {
query = query.Where("task_id = ?", *options.TaskID)
}
@@ -87,7 +87,7 @@ func (r *GormBackupRecordRepository) List(ctx context.Context, options BackupRec
func (r *GormBackupRecordRepository) FindByID(ctx context.Context, id uint) (*model.BackupRecord, error) {
var item model.BackupRecord
if err := r.db.WithContext(ctx).Preload("Task").Preload("StorageTarget").Preload("Task.StorageTarget").First(&item, id).Error; err != nil {
if err := r.db.WithContext(ctx).Preload("Task").Preload("Task.StorageTarget").First(&item, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
@@ -115,9 +115,7 @@ func (r *GormBackupRecordRepository) Create(ctx context.Context, item *model.Bac
}
func (r *GormBackupRecordRepository) Update(ctx context.Context, item *model.BackupRecord) error {
// Task 与 StorageTarget 是查询时预加载的只读关联。更新记录字段时忽略它们,
// 避免已加载的旧关联把刚修改的外键(例如首个成功上传目标)覆盖回去。
return r.db.WithContext(ctx).Omit("Task", "StorageTarget").Save(item).Error
return r.db.WithContext(ctx).Save(item).Error
}
func (r *GormBackupRecordRepository) Delete(ctx context.Context, id uint) error {
@@ -129,7 +127,7 @@ func (r *GormBackupRecordRepository) ListRecent(ctx context.Context, limit int)
limit = 10
}
var items []model.BackupRecord
if err := r.db.WithContext(ctx).Omit("Manifest").Preload("Task").Preload("StorageTarget").Preload("Task.StorageTarget").Order("started_at desc").Limit(limit).Find(&items).Error; err != nil {
if err := r.db.WithContext(ctx).Omit("Manifest").Preload("Task").Preload("Task.StorageTarget").Order("started_at desc").Limit(limit).Find(&items).Error; err != nil {
return nil, err
}
return items, nil
@@ -65,9 +65,6 @@ func TestBackupRecordRepositoryQueries(t *testing.T) {
if stored == nil || stored.FileName != "website.tar.gz" {
t.Fatalf("unexpected stored record: %#v", stored)
}
if stored.StorageTarget.Name != "local" {
t.Fatalf("expected record storage target to be preloaded, got %#v", stored.StorageTarget)
}
listed, err := repo.List(ctx, BackupRecordListOptions{TaskID: &record.TaskID, Status: "success"})
if err != nil {
t.Fatalf("List returned error: %v", err)
@@ -75,9 +72,6 @@ func TestBackupRecordRepositoryQueries(t *testing.T) {
if len(listed) != 1 {
t.Fatalf("expected one listed record, got %d", len(listed))
}
if listed[0].StorageTarget.Name != "local" {
t.Fatalf("expected listed storage target to be preloaded, got %#v", listed[0].StorageTarget)
}
recent, err := repo.ListRecent(ctx, 5)
if err != nil {
t.Fatalf("ListRecent returned error: %v", err)
@@ -85,24 +79,6 @@ func TestBackupRecordRepositoryQueries(t *testing.T) {
if len(recent) != 1 {
t.Fatalf("expected one recent record, got %d", len(recent))
}
if recent[0].StorageTarget.Name != "local" {
t.Fatalf("expected recent storage target to be preloaded, got %#v", recent[0].StorageTarget)
}
secondTarget := &model.StorageTarget{Name: "secondary", Type: "local_disk", Enabled: true, ConfigCiphertext: "{}", ConfigVersion: 1, LastTestStatus: "unknown"}
if err := repo.db.Create(secondTarget).Error; err != nil {
t.Fatalf("seed second storage target error: %v", err)
}
stored.StorageTargetID = secondTarget.ID
if err := repo.Update(ctx, stored); err != nil {
t.Fatalf("Update storage target ID returned error: %v", err)
}
updated, err := repo.FindByID(ctx, record.ID)
if err != nil {
t.Fatalf("FindByID after update returned error: %v", err)
}
if updated == nil || updated.StorageTargetID != secondTarget.ID || updated.StorageTarget.Name != "secondary" {
t.Fatalf("expected updated storage target to remain %d, got %#v", secondTarget.ID, updated)
}
total, err := repo.Count(ctx)
if err != nil {
t.Fatalf("Count returned error: %v", err)
@@ -1026,8 +1026,7 @@ func (s *BackupExecutionService) executeTask(ctx context.Context, task *model.Ba
return
}
defer os.RemoveAll(result.TempDir)
// 依据运行器产出判定实际类型:文件全量会产出清单;只有明确启用差异模式且
// 未产出清单时才是差异备份。数据库运行器本身不产出文件清单,但仍属于全量。
// 依据运行器产出判定实际类型:产出清单 → 全量(记录清单供后续差异比对);否则为差异。
if result.Manifest != nil {
backupKind = model.BackupKindFull
if data, encErr := backup.EncodeManifest(*result.Manifest); encErr == nil {
@@ -1035,10 +1034,8 @@ func (s *BackupExecutionService) executeTask(ctx context.Context, task *model.Ba
} else {
logger.Warnf("备份清单序列化失败(不影响本次备份,但将禁用后续差异):%v", encErr)
}
} else if spec.Differential {
backupKind = model.BackupKindDifferential
} else {
backupKind = model.BackupKindFull
backupKind = model.BackupKindDifferential
}
finalPath := result.ArtifactPath
if strings.EqualFold(task.Compression, "gzip") && !strings.HasSuffix(strings.ToLower(finalPath), ".gz") {
@@ -108,11 +108,6 @@ func newExecutionTestServices(t *testing.T) (*BackupExecutionService, *BackupRec
if err != nil {
t.Fatalf("database.Open returned error: %v", err)
}
sqlDB, err := db.DB()
if err != nil {
t.Fatalf("db.DB returned error: %v", err)
}
t.Cleanup(func() { _ = sqlDB.Close() })
cipher := codec.NewConfigCipher("execution-secret")
tasks := repository.NewBackupTaskRepository(db)
targets := repository.NewStorageTargetRepository(db)
@@ -161,34 +156,6 @@ func TestBackupExecutionServiceRunTaskByIDSync(t *testing.T) {
}
}
func TestBackupExecutionServiceSQLiteBackupRemainsFull(t *testing.T) {
executionService, _, tasks, _, records, sourceDir, _ := newExecutionTestServices(t)
dbPath := filepath.Join(sourceDir, "finance.db")
if err := os.WriteFile(dbPath, []byte("sqlite-demo"), 0o644); err != nil {
t.Fatalf("WriteFile sqlite fixture returned error: %v", err)
}
task := &model.BackupTask{
Name: "finance-db", Type: model.BackupTaskTypeSQLite, Enabled: true,
DBPath: dbPath, StorageTargetID: 1, RetentionDays: 30,
Compression: "none", MaxBackups: 10, LastStatus: "idle",
}
if err := tasks.Create(context.Background(), task); err != nil {
t.Fatalf("Create sqlite task returned error: %v", err)
}
detail, err := executionService.RunTaskByIDSync(context.Background(), task.ID)
if err != nil {
t.Fatalf("RunTaskByIDSync sqlite returned error: %v", err)
}
stored, err := records.FindByID(context.Background(), detail.ID)
if err != nil {
t.Fatalf("FindByID sqlite record returned error: %v", err)
}
if stored == nil || stored.BackupKind != model.BackupKindFull {
t.Fatalf("expected sqlite backup kind full, got %#v", stored)
}
}
func TestBackupExecutionServiceRepositoryModeRoundTrip(t *testing.T) {
executionService, recordService, tasks, _, records, sourceDir, storageDir := newExecutionTestServices(t)
ctx := context.Background()
+6 -6
View File
@@ -8,11 +8,10 @@ import (
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"backupx/server/internal/config"
"github.com/shirou/gopsutil/v4/disk"
)
type SystemInfo struct {
@@ -125,10 +124,11 @@ func (s *SystemService) GetInfo(_ context.Context) *SystemInfo {
if dir == "" {
dir = "."
}
if stat, err := disk.Usage(dir); err == nil {
info.DiskTotal = int64(stat.Total)
info.DiskFree = int64(stat.Free)
info.DiskUsed = int64(stat.Used)
var stat syscall.Statfs_t
if err := syscall.Statfs(dir, &stat); err == nil {
info.DiskTotal = int64(stat.Blocks) * int64(stat.Bsize)
info.DiskFree = int64(stat.Bavail) * int64(stat.Bsize)
info.DiskUsed = info.DiskTotal - info.DiskFree
}
return info
}