| 1 | const MINUTE = 60_000; |
| 2 | const HOUR = 60 * MINUTE; |
| 3 | const DAY = 24 * HOUR; |
| 4 | |
| 5 | export function formatScheduledTaskInterval(everyMs: number): string { |
| 6 | const units = [ |
| 7 | [7 * DAY, "week"], |
| 8 | [DAY, "day"], |
| 9 | [HOUR, "hour"], |
| 10 | [MINUTE, "minute"], |
| 11 | [1_000, "second"], |
| 12 | [1, "millisecond"], |
| 13 | ] as const; |
| 14 | let remaining = everyMs; |
| 15 | const parts: string[] = []; |
| 16 | for (const [size, unit] of units) { |
| 17 | const count = Math.floor(remaining / size); |
| 18 | if (count === 0) continue; |
| 19 | if (count === 1 && remaining === everyMs && remaining === size) return `Every ${unit}`; |
| 20 | parts.push(`${count} ${unit}${count === 1 ? "" : "s"}`); |
| 21 | remaining %= size; |
| 22 | } |
| 23 | return `Every ${parts.join(" ")}`; |
| 24 | } |
| 25 | |
| 26 | function localCalendarDay(date: Date): number { |
| 27 | // Compare calendar days, not 24-hour spans: DST days can have 23 or 25 hours. |
| 28 | return Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) / DAY; |
| 29 | } |
| 30 | |
| 31 | export function formatNextScheduledTaskRun(nextRunAt: string, now: number): string { |
| 32 | const next = new Date(nextRunAt); |
| 33 | const current = new Date(now); |
| 34 | const remaining = next.getTime() - now; |
| 35 | if (!Number.isFinite(remaining)) return "Next run unavailable"; |
| 36 | if (remaining <= 0) return "Next run due"; |
| 37 | |
| 38 | const days = localCalendarDay(next) - localCalendarDay(current); |
| 39 | if (days === 0) { |
| 40 | if (remaining < MINUTE) return "Next run in less than a minute"; |
| 41 | const unit = remaining < HOUR ? "minute" : "hour"; |
| 42 | const count = Math.round(remaining / (remaining < HOUR ? MINUTE : HOUR)); |
| 43 | return `Next run in ${count} ${unit}${count === 1 ? "" : "s"}`; |
| 44 | } |
| 45 | |
| 46 | const time = next.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }); |
| 47 | if (days === 1) return `Next run tomorrow at ${time}`; |
| 48 | if (days <= 7) { |
| 49 | return `Next run next ${next.toLocaleDateString([], { weekday: "long" })} at ${time}`; |
| 50 | } |
| 51 | const date = next.toLocaleDateString([], { |
| 52 | month: "short", |
| 53 | day: "numeric", |
| 54 | ...(next.getFullYear() !== current.getFullYear() ? { year: "numeric" as const } : {}), |
| 55 | }); |
| 56 | return `Next run ${date} at ${time}`; |
| 57 | } |