diff --git a/internal/server/http/catchup.go b/internal/server/http/catchup.go index 68f7d92..9c67def 100644 --- a/internal/server/http/catchup.go +++ b/internal/server/http/catchup.go @@ -42,9 +42,6 @@ const catchupSettle = 60 * time.Second func (s *Server) ArmCatchup(hostID string, now time.Time) { s.catchupMu.Lock() defer s.catchupMu.Unlock() - if s.catchupDueAt == nil { - s.catchupDueAt = make(map[string]time.Time) - } s.catchupDueAt[hostID] = now.Add(catchupSettle) } @@ -100,6 +97,13 @@ func (s *Server) runCatchup(ctx context.Context, hostID string, now time.Time) { slog.Warn("catchup: list schedules", "host_id", hostID, "err", err) return } + // NOTE: overdue is measured against host.LastBackupAt, which is the + // most recent *successful backup of any schedule* on this host — not + // a per-schedule timestamp. For the common intermittent host (a + // single backup schedule) this is exact. With multiple schedules of + // different cadences, a recent backup from one schedule can mask + // another schedule's missed window. Acceptable for v1; revisit with + // per-schedule last-success tracking if multi-cadence laptops appear. for _, sc := range schedules { if !sc.Enabled || len(sc.SourceGroupIDs) == 0 { continue @@ -115,8 +119,10 @@ func (s *Server) runCatchup(ctx context.Context, hostID string, now time.Time) { continue } if _, derr := s.dispatchBackupForGroupCore(ctx, conn, hostID, sc.ID, g, now); derr != nil { - // Send failed — host dropped again. Re-arm so the next - // reconnect retries; stop processing this host. + // Send failed for this group — host may have dropped + // again. Earlier groups in this batch were already + // dispatched; re-arm so a later reconnect re-evaluates + // any still-overdue schedules. s.ArmCatchup(hostID, now) return }