yiekheng e6521bd151 feat(web): run-eta helper + bigger pill-shaped add buttons
estimateRunDuration() computes a per-run ETA from a target count, a
fire time, and an assumed per-account send rate (40/min, mirroring
the bot env). Adds a 15% buffer with a 1-minute floor. Pure helper,
covered by 6 round-trip tests including the rate-defaults path.

Header CTA buttons on /accounts and /reminders are now size="lg"
rounded-full pills with a shadow that lifts on hover. Mobile shows
just the plus icon (label collapses) so the button doesn't dominate
narrow screens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:10:09 +08:00

40 lines
1.4 KiB
TypeScript

/**
* Default per-account send rate, mirroring `BOT_MAX_SEND_PER_MINUTE`
* in the bot env. The web bundle hardcodes this — operators who tune
* the bot env are expected to redeploy web with the matching value.
*/
export const ASSUMED_RATE_PER_MINUTE = 40;
const ETA_BUFFER = 1.15;
/**
* Pure ETA helper. Given a target count and a fire time, returns the
* estimated duration in whole minutes and the projected finish
* timestamp.
*
* Calculation:
* ceil((targetCount / ratePerMinute) * 1.15) minutes
* estimatedFinishAt = fireAt + that many minutes
*
* Floor of 1 minute when targetCount > 0 (anything non-zero takes at
* least a minute to feel real). Returns 0 minutes when targetCount
* is zero — the run is a no-op.
*/
export function estimateRunDuration(opts: {
targetCount: number;
ratePerMinute?: number;
fireAt: Date;
}): { durationMinutes: number; estimatedFinishAt: Date } {
const rate = opts.ratePerMinute ?? ASSUMED_RATE_PER_MINUTE;
if (rate <= 0) throw new Error("ratePerMinute must be > 0");
if (opts.targetCount <= 0) {
return { durationMinutes: 0, estimatedFinishAt: new Date(opts.fireAt) };
}
const raw = (opts.targetCount / rate) * ETA_BUFFER;
const durationMinutes = Math.max(1, Math.ceil(raw));
const estimatedFinishAt = new Date(
opts.fireAt.getTime() + durationMinutes * 60_000,
);
return { durationMinutes, estimatedFinishAt };
}