package postgres import ( "context" "errors" "time" "github.com/jackc/pgx/v5" "knowledge-governance/internal/governance" ) // LoadBackupSchedule reads the single active backup schedule row. A missing row // (fresh database before the seed, or a rolled-back migration) reads back as the // defaults instead of an error, so the settings page never hard-fails. func (s *Store) LoadBackupSchedule(ctx context.Context) (governance.BackupSchedule, error) { schedule := governance.BackupSchedule{ ID: "active", Enabled: false, ScheduleTime: governance.BackupScheduleDefaultTime, RetentionDays: 7, RetentionMonths: 3, } err := s.db.QueryRow(ctx, ` SELECT enabled,schedule_time,retention_days,retention_months,last_applied_at,last_applied_summary,updated_by,updated_at FROM backup_schedule WHERE id='active'`). Scan(&schedule.Enabled, &schedule.ScheduleTime, &schedule.RetentionDays, &schedule.RetentionMonths, &schedule.LastAppliedAt, &schedule.LastAppliedSummary, &schedule.UpdatedBy, &schedule.UpdatedAt) if errors.Is(err, pgx.ErrNoRows) { return schedule, nil } if err != nil { return governance.BackupSchedule{}, errors.New("load backup schedule") } return schedule, nil } // SaveBackupSchedule upserts the schedule row. Validation belongs to the caller // (governance.BackupSchedule.Validate); this only persists what was accepted. func (s *Store) SaveBackupSchedule(ctx context.Context, schedule governance.BackupSchedule, updatedBy string, now time.Time) error { if now.IsZero() { return errors.New("backup schedule save requires a timestamp") } if _, err := s.db.Exec(ctx, ` INSERT INTO backup_schedule(id,enabled,schedule_time,retention_days,retention_months,updated_by,updated_at) VALUES('active',$1,$2,$3,$4,$5,$6) ON CONFLICT(id) DO UPDATE SET enabled=EXCLUDED.enabled, schedule_time=EXCLUDED.schedule_time, retention_days=EXCLUDED.retention_days, retention_months=EXCLUDED.retention_months, updated_by=EXCLUDED.updated_by, updated_at=EXCLUDED.updated_at`, schedule.Enabled, schedule.ScheduleTime, schedule.RetentionDays, schedule.RetentionMonths, updatedBy, now); err != nil { return errors.New("save backup schedule") } return nil } // MarkBackupScheduleApplied records the last successful push to the data-plane // host (cron file + next run), which the page shows as "当前主机上的计划". func (s *Store) MarkBackupScheduleApplied(ctx context.Context, summary string, appliedAt time.Time) error { if appliedAt.IsZero() { return errors.New("backup schedule apply requires a timestamp") } if _, err := s.db.Exec(ctx, ` UPDATE backup_schedule SET last_applied_at=$1, last_applied_summary=$2 WHERE id='active'`, appliedAt, summary); err != nil { return errors.New("mark backup schedule applied") } return nil }