diff --git a/internal/api/server.go b/internal/api/server.go index 8a62f615..0d249bc6 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -193,11 +193,9 @@ func (s *Server) GetOperationEvents(ctx context.Context, req *connect.Request[em return } - go func() { - if err := resp.Send(event); err != nil { - errorChan <- fmt.Errorf("failed to send event: %w", err) - } - }() + if err := resp.Send(event); err != nil { + errorChan <- fmt.Errorf("failed to send event: %w", err) + } } s.oplog.Subscribe(&callback) defer s.oplog.Unsubscribe(&callback) diff --git a/internal/orchestrator/repo.go b/internal/orchestrator/repo.go index 460b49d1..b883f712 100644 --- a/internal/orchestrator/repo.go +++ b/internal/orchestrator/repo.go @@ -85,7 +85,7 @@ func (r *RepoOrchestrator) Backup(ctx context.Context, plan *v1.Plan, progressCa summary, err := r.repo.Backup(ctx, progressCallback, opts...) if err != nil { - return nil, fmt.Errorf("failed to backup: %w", err) + return summary, fmt.Errorf("failed to backup: %w", err) } r.l.Debug("Backup completed", zap.String("repo", r.repoConfig.Id), zap.Duration("duration", time.Since(startTime))) @@ -115,16 +115,12 @@ func (r *RepoOrchestrator) Forget(ctx context.Context, plan *v1.Plan) ([]*v1.Res return nil, fmt.Errorf("plan %q has no retention policy", plan.Id) } - l := r.l.With(zap.String("plan", plan.Id)) - - l.Debug("Forget snapshots", zap.Any("policy", policy)) result, err := r.repo.Forget( ctx, protoutil.RetentionPolicyFromProto(plan.Retention), restic.WithFlags("--tag", tagForPlan(plan)), restic.WithFlags("--group-by", "tag")) if err != nil { return nil, fmt.Errorf("get snapshots for repo %v: %w", r.repoConfig.Id, err) } - l.Debug("Forget result", zap.Any("result", result)) var forgotten []*v1.ResticSnapshot for _, snapshot := range result.Remove { @@ -135,6 +131,8 @@ func (r *RepoOrchestrator) Forget(ctx context.Context, plan *v1.Plan) ([]*v1.Res forgotten = append(forgotten, snapshotProto) } + zap.L().Debug("Forgot snapshots", zap.String("plan", plan.Id), zap.Int("count", len(forgotten)), zap.Any("policy", policy)) + return forgotten, nil } diff --git a/pkg/restic/restic.go b/pkg/restic/restic.go index 8c10414b..1e4501c4 100644 --- a/pkg/restic/restic.go +++ b/pkg/restic/restic.go @@ -137,7 +137,7 @@ func (r *Repo) Backup(ctx context.Context, progressCallback func(*BackupProgress if err := cmd.Wait(); err != nil { var exitErr *exec.ExitError if errors.As(err, &exitErr) { - if exitErr.ExitCode() == 1 { + if exitErr.ExitCode() == 3 { cmdErr = ErrPartialBackup } else { cmdErr = fmt.Errorf("exit code %v: %w", exitErr.ExitCode(), ErrBackupFailed) @@ -151,7 +151,7 @@ func (r *Repo) Backup(ctx context.Context, progressCallback func(*BackupProgress wg.Wait() if cmdErr != nil || readErr != nil { - return nil, newCmdErrorPreformatted(cmd, output.String(), errors.Join(cmdErr, readErr)) + return summary, newCmdErrorPreformatted(cmd, output.String(), errors.Join(cmdErr, readErr)) } return summary, nil diff --git a/webui/src/components/OperationList.tsx b/webui/src/components/OperationList.tsx index 0f4c862c..b8c1e2b0 100644 --- a/webui/src/components/OperationList.tsx +++ b/webui/src/components/OperationList.tsx @@ -126,7 +126,8 @@ export const OperationList = ({ size="small" dataSource={backups} renderItem={(backup) => { - const ops = backup.operations; + const ops = [...backup.operations]; + ops.reverse(); return ( {ops.map((op) => { diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index 90e9e92f..3febbde7 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from "react"; import { BackupInfo, BackupInfoCollector, + colorForStatus, displayTypeToString, getOperations, getTypeForDisplay, @@ -260,27 +261,9 @@ const buildTreeDay = (keyPrefix: string, operations: BackupInfo[]): OpTreeNode[] const buildTreeLeaf = (operations: BackupInfo[]): OpTreeNode[] => { const entries = _.map(operations, (b): OpTreeNode => { - let iconColor = "grey"; + let iconColor = colorForStatus(b.status); let icon: React.ReactNode | null = ; - switch (b.status) { - case OperationStatus.STATUS_PENDING: - iconColor = "grey"; - break; - case OperationStatus.STATUS_SUCCESS: - iconColor = "green"; - break; - case OperationStatus.STATUS_ERROR: - iconColor = "red"; - break; - case OperationStatus.STATUS_INPROGRESS: - iconColor = "blue"; - break; - case OperationStatus.STATUS_USER_CANCELLED: - iconColor = "orange"; - break; - } - if (b.status === OperationStatus.STATUS_ERROR) { icon = ; } else { diff --git a/webui/src/state/oplog.ts b/webui/src/state/oplog.ts index 8a7df1e8..dee5dbff 100644 --- a/webui/src/state/oplog.ts +++ b/webui/src/state/oplog.ts @@ -91,7 +91,7 @@ export class BackupInfoCollector { private createBackup(operations: Operation[]): BackupInfo { // deduplicate and sort operations. operations.sort((a, b) => { - return Number(b.unixTimeStartMs - a.unixTimeStartMs); + return Number(a.unixTimeStartMs - b.unixTimeStartMs); }); // use the lowest ID of all operations as the ID of the backup, this will be the first created operation. @@ -258,6 +258,26 @@ export const displayTypeToString = (type: DisplayType) => { return "Unknown"; } }; + +export const colorForStatus = (status: OperationStatus) => { + switch (status) { + case OperationStatus.STATUS_PENDING: + return "grey"; + case OperationStatus.STATUS_INPROGRESS: + return "blue"; + case OperationStatus.STATUS_ERROR: + return "red"; + case OperationStatus.STATUS_WARNING: + return "orange"; + case OperationStatus.STATUS_SUCCESS: + return "green"; + case OperationStatus.STATUS_USER_CANCELLED: + return "orange"; + default: + return "grey"; + } +} + // detailsForOperation returns derived display information for a given operation. export const detailsForOperation = ( op: Operation diff --git a/webui/src/views/AddPlanModal.tsx b/webui/src/views/AddPlanModal.tsx index 40523014..dee617f2 100644 --- a/webui/src/views/AddPlanModal.tsx +++ b/webui/src/views/AddPlanModal.tsx @@ -49,7 +49,6 @@ export const AddPlanModal = ({ try { let config = await fetchConfig(); - config.plans = config.plans || []; if (!template) { throw new Error("template not found"); @@ -82,10 +81,9 @@ export const AddPlanModal = ({ setConfirmLoading(true); try { - let plan = await validateForm(form); + let plan = new Plan(await validateForm(form)); let config = await fetchConfig(); - config.plans = config.plans || []; // Merge the new plan (or update) into the config if (template) { @@ -103,6 +101,7 @@ export const AddPlanModal = ({ showModal(null); } catch (e: any) { alertsApi.error("Operation failed: " + e.message, 15); + console.error(e); } finally { setConfirmLoading(false); }