Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ desktop.ini
.env.local
*.env

# ─── AI Assistants ────────────────────────────────────────────────────────────

.claude
CLAUDE.md

# ─── Helm ─────────────────────────────────────────────────────────────────────
# Subchart tarballs are regenerated with `helm dependency update`; only Chart.lock is committed.
deploy/helm/**/charts/*.tgz
Expand Down
3 changes: 2 additions & 1 deletion gui/src/components/policies/PolicySheet.vue
Original file line number Diff line number Diff line change
Expand Up @@ -668,9 +668,10 @@ const onSubmit = handleSubmit(async (values) => {
}

if (isEdit.value) {
// PATCH-only: enabled, optional new password
// PATCH-only: enabled, optional new password, destinations
body.enabled = values.enabled
if (values.repo_password) body.repo_password = values.repo_password
body.destinations = destinationsPayload
} else {
// POST-only: agent, password (required), destinations
body.agent_id = values.agent_id
Expand Down
56 changes: 45 additions & 11 deletions server/internal/api/policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,18 +319,20 @@ func (h *PolicyHandler) GetByID(w http.ResponseWriter, r *http.Request) {

// updatePolicyRequest is the JSON body for PATCH /api/v1/policies/{id}.
// All fields are optional — only non-nil values are applied.
// Destinations replaces the full set when non-empty; omitting it leaves destinations unchanged.
type updatePolicyRequest struct {
Name *string `json:"name"`
Schedule *string `json:"schedule"`
Enabled *bool `json:"enabled"`
Sources *string `json:"sources"`
RepoPassword *string `json:"repo_password"`
RetentionDaily *int `json:"retention_daily"`
RetentionWeekly *int `json:"retention_weekly"`
RetentionMonthly *int `json:"retention_monthly"`
RetentionYearly *int `json:"retention_yearly"`
HookPreBackup *string `json:"hook_pre_backup"`
HookPostBackup *string `json:"hook_post_backup"`
Name *string `json:"name"`
Schedule *string `json:"schedule"`
Enabled *bool `json:"enabled"`
Sources *string `json:"sources"`
RepoPassword *string `json:"repo_password"`
RetentionDaily *int `json:"retention_daily"`
RetentionWeekly *int `json:"retention_weekly"`
RetentionMonthly *int `json:"retention_monthly"`
RetentionYearly *int `json:"retention_yearly"`
HookPreBackup *string `json:"hook_pre_backup"`
HookPostBackup *string `json:"hook_post_backup"`
Destinations []destinationEntryRequest `json:"destinations"`
}

// Update handles PATCH /api/v1/policies/{id}.
Expand Down Expand Up @@ -423,6 +425,38 @@ func (h *PolicyHandler) Update(w http.ResponseWriter, r *http.Request) {
policy.HookPostBackup = *req.HookPostBackup
}

if len(req.Destinations) > 0 {
if err := h.repo.DeleteAllDestinations(r.Context(), id); err != nil {
h.logger.Error("failed to remove destinations on update", zap.String("id", id.String()), zap.Error(err))
ErrInternal(w)
return
}
for _, d := range req.Destinations {
destID, err := uuid.Parse(d.DestinationID)
if err != nil {
ErrBadRequest(w, "invalid destination_id: "+d.DestinationID)
return
}
pd := &db.PolicyDestination{
PolicyID: id,
DestinationID: destID,
Priority: d.Priority,
}
if err := h.repo.AddDestination(r.Context(), pd); err != nil {
h.logger.Error("failed to add destination on update", zap.String("id", id.String()), zap.Error(err))
ErrInternal(w)
return
}
}
var err error
destinations, err = h.repo.GetDestinations(r.Context(), id)
if err != nil {
h.logger.Error("failed to reload destinations after update", zap.String("id", id.String()), zap.Error(err))
ErrInternal(w)
return
}
}

if err := h.repo.Update(r.Context(), policy); err != nil {
h.logger.Error("failed to update policy", zap.String("id", id.String()), zap.Error(err))
ErrInternal(w)
Expand Down
18 changes: 18 additions & 0 deletions server/internal/repositories/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,24 @@ func (r *gormPolicyRepository) RemoveDestination(ctx context.Context, policyID,
return nil
}

// DeleteAllDestinations removes all destination associations for a policy.
// Used during policy update to replace the full set of destinations.
func (r *gormPolicyRepository) DeleteAllDestinations(ctx context.Context, policyID uuid.UUID) error {
return r.db.WithContext(ctx).
Where("policy_id = ?", policyID).
Delete(&db.PolicyDestination{}).Error
}

// GetDestinations returns all destination associations for a policy ordered by priority.
func (r *gormPolicyRepository) GetDestinations(ctx context.Context, policyID uuid.UUID) ([]db.PolicyDestination, error) {
var dests []db.PolicyDestination
err := r.db.WithContext(ctx).
Where("policy_id = ?", policyID).
Order("priority ASC").
Find(&dests).Error
return dests, err
}

// UpdateDestinationPriority updates the priority of a destination within a policy.
// Lower priority values are tried first during backup execution.
func (r *gormPolicyRepository) UpdateDestinationPriority(ctx context.Context, policyID, destinationID uuid.UUID, priority int) error {
Expand Down
2 changes: 2 additions & 0 deletions server/internal/repositories/repositories.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ type PolicyRepository interface {
AddDestination(ctx context.Context, pd *db.PolicyDestination) error
RemoveDestination(ctx context.Context, policyID, destinationID uuid.UUID) error
UpdateDestinationPriority(ctx context.Context, policyID, destinationID uuid.UUID, priority int) error
DeleteAllDestinations(ctx context.Context, policyID uuid.UUID) error
GetDestinations(ctx context.Context, policyID uuid.UUID) ([]db.PolicyDestination, error)
}

// -----------------------------------------------------------------------------
Expand Down
Loading