warmbox/docs/sprints/sprint-1.2-1.3.md

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`