81 lines
3.1 KiB
Markdown
81 lines
3.1 KiB
Markdown
# Sprint 1.2 + 1.3 — Recipes Complete + Campaign Dispatcher
|
|
|
|
Combined sprint delivering all four warmup recipes and live campaign-aware sending.
|
|
|
|
## What shipped
|
|
|
|
### Sprint 1.2 — Recipe engine (complete)
|
|
|
|
- **Grow**, **Flat**, **Randomized**, and **Custom** recipes via `generateSchedule()` in `recipe.service.ts`
|
|
- Randomized uses a seeded PRNG (`campaignId` at start) plus the 48h volume cap
|
|
- Custom validates per-day sends in `[min, max]` and fills gaps with hold-last
|
|
- `POST /api/recipes/preview` accepts all four recipes (optional `customSchedule` for custom)
|
|
- `GET /api/recipes/templates` returns templates for all four recipes
|
|
- `POST /api/campaigns/:id/start` persists the correct schedule per recipe
|
|
|
|
### Sprint 1.3 — Campaign dispatcher (complete)
|
|
|
|
- **`campaign-dispatcher.job.ts`** — satellite → primary sends for active/maintenance campaigns
|
|
- **`campaign-rollover.job.ts`** + **`syncCampaignDay()`** — day rollover and maintenance transition
|
|
- **`send-window.ts`** — timezone-aware send windows (luxon)
|
|
- **`lastSendAt`** on `WarmupCampaign` — 45 min minimum jitter between sends
|
|
- Campaign-aware **rescuer** — primary → satellite replies with `actualReplies` tracking
|
|
- Manual job endpoints: `POST /api/jobs/dispatcher/run`, `POST /api/jobs/rollover/run`
|
|
- Legacy flat dispatcher still runs when **zero** campaigns exist
|
|
|
|
## Traffic rules
|
|
|
|
| Direction | Job |
|
|
| ------------------- | --------- |
|
|
| Satellite → Primary | Dispatcher |
|
|
| Primary → Satellite | Rescuer (reply only) |
|
|
|
|
## Scheduler
|
|
|
|
| Job | Schedule |
|
|
| ---------------------- | --------------------- |
|
|
| Campaign dispatcher | `dispatcher_cron` setting |
|
|
| Legacy dispatcher | Same cron (gated) |
|
|
| Rescuer | `rescuer_cron` setting |
|
|
| Campaign rollover | Hourly (`0 * * * *`) |
|
|
|
|
## Manual QA
|
|
|
|
```bash
|
|
# 1. Create primary + 2+ satellites (see sprint-1.1.md)
|
|
# 2. Create campaign (recipe: grow), assign satellites, start
|
|
# 3. Trigger dispatch:
|
|
curl -X POST http://localhost:3000/api/jobs/dispatcher/run -H "x-api-key: $API_KEY"
|
|
# 4. Check logs:
|
|
curl http://localhost:3000/api/logs?inboxId=<primary_id> -H "x-api-key: $API_KEY"
|
|
# 5. Wait for rescuer cron or trigger rescuer manually via cron interval
|
|
# 6. Repeat with recipe:flat on a second primary to verify recipe router
|
|
```
|
|
|
|
## Preview examples
|
|
|
|
```bash
|
|
# Flat 30-day preview
|
|
curl -X POST http://localhost:3000/api/recipes/preview \
|
|
-H "Content-Type: application/json" -H "x-api-key: $API_KEY" \
|
|
-d '{"recipe":"flat","durationDays":30,"maxEmailsPerDay":40}'
|
|
|
|
# Custom schedule
|
|
curl -X POST http://localhost:3000/api/recipes/preview \
|
|
-H "Content-Type: application/json" -H "x-api-key: $API_KEY" \
|
|
-d '{"recipe":"custom","durationDays":14,"customSchedule":[{"day":1,"sends":2},{"day":2,"sends":5}]}'
|
|
```
|
|
|
|
## Deferred to Sprint 1.4
|
|
|
|
- Probabilistic reply rate (~30% sampling)
|
|
- `engaged_no_reply` status
|
|
|
|
## Key files
|
|
|
|
- `src/services/recipe.service.ts`
|
|
- `src/services/campaign.service.ts`
|
|
- `src/jobs/campaign-dispatcher.job.ts`
|
|
- `src/jobs/campaign-rollover.job.ts`
|
|
- `src/jobs/rescuer.job.ts`
|
|
- `src/lib/send-window.ts`
|