diff --git a/internal/app/consumer/warmup_reply_back.go b/internal/app/consumer/warmup_reply_back.go index bce30a1b3..385692d4c 100644 --- a/internal/app/consumer/warmup_reply_back.go +++ b/internal/app/consumer/warmup_reply_back.go @@ -49,8 +49,12 @@ func (s *JobsService) scheduleWarmupReplyBack(ctx context.Context, token *models time.Duration(uint64(binary.BigEndian.Uint32(token.Token[4:8]))%uint64(replyBackMaxDelay-replyBackMinDelay))) // A reply at 04:00 from a mailbox that never sends then is worse than none. if rolled := withinWarmupHours(at, recipient); !rolled.Equal(at) { - // Spread replies rolled to the opening over its first hour, so the pool does not answer on the stroke of it. - at = rolled.Add(time.Duration(binary.BigEndian.Uint32(token.Token[8:12])%3600) * time.Second) + // Spread replies rolled to the opening over its first hour (or the whole window when shorter), so the pool does not answer on the stroke of it. + spread := uint32(3600) + if span := models.ClockMinutes(recipient.WarmupEndTime, 20*60) - models.ClockMinutes(recipient.WarmupStartTime, 8*60); span > 0 && span*60 < int(spread) { + spread = uint32(span * 60) + } + at = rolled.Add(time.Duration(binary.BigEndian.Uint32(token.Token[8:12])%spread) * time.Second) } moved, err := lineage.BindWarmupSuccessor(ctx, token.TaskID, recipientAccountID, receivedIDs[0], at) diff --git a/internal/tasks/email_task.go b/internal/tasks/email_task.go index 93af32c9f..dbc24e7a1 100644 --- a/internal/tasks/email_task.go +++ b/internal/tasks/email_task.go @@ -245,8 +245,11 @@ func (s *tasksService) HandleEmailTask(task *proto.ProcessTask) *errx.Error { return errx.InternalError() } if at.After(time.Now()) { - // The gate answers with the window's opening; spread deferred sends past it rather than firing on the stroke. - at = at.Add(time.Duration(1+rand.Intn(20*60)) * time.Second) + // The gate answers with the window's opening; spread deferred sends past it, unless that would leave a short window. + jittered := at.Add(time.Duration(1+rand.Intn(20*60)) * time.Second) + if again, gerr := gate.WarmupDispatchNotBefore(ctx, account.ID, jittered); gerr == nil && !again.After(jittered) { + at = jittered + } if err := lineage.RescheduleWarmupTask(ctx, taskID, at); err != nil { return errx.InternalError() } diff --git a/web/src/lib/helper/nextSlot.ts b/web/src/lib/helper/nextSlot.ts index 22c293703..72d6ea595 100644 --- a/web/src/lib/helper/nextSlot.ts +++ b/web/src/lib/helper/nextSlot.ts @@ -1,10 +1,11 @@ -// A queued send time as a short label: "10:08" today, "tomorrow 09:12", or "Mon 09:12" later in the week. +// A queued send time as a short label: "10:08" today, "tomorrow 09:12", "Mon 09:12" later in the week, and a dated label otherwise (an overdue slot included). export function nextSlotLabel(at: Date | string, now: Date = new Date()): string { const d = new Date(at); const time = d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); const day = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime(); const days = Math.round((day(d) - day(now)) / 86_400_000); - if (days <= 0) return time; + if (days === 0) return time; + if (days < 0) return `${d.toLocaleDateString([], { month: "short", day: "numeric" })} ${time}`; if (days === 1) return `tomorrow ${time}`; if (days < 7) return `${d.toLocaleDateString([], { weekday: "short" })} ${time}`; return `${d.toLocaleDateString([], { month: "short", day: "numeric" })} ${time}`;