# cPanel Cron Setup

The whole engine (campaign sending, import processing, queued jobs, retention pruning)
runs from **one** cPanel cron entry.

## 1. Find the PHP binary

Over SSH / cPanel Terminal:

```bash
which php && php -v
ls /usr/local/bin/ea-php*
```

Use plain `php` if it reports 8.2/8.3; otherwise use the versioned binary,
e.g. `/usr/local/bin/ea-php83`.

## 2. Add the cron job

cPanel → **Cron Jobs** → Add New Cron Job:

- **Common Settings:** Once Per Minute (`* * * * *`)
- **Command:**

```cron
* * * * * /usr/local/bin/php /home/CPANEL_USER/fxt-crm-app/artisan schedule:run >> /home/CPANEL_USER/fxt-crm-cron.log 2>&1
```

Replace the PHP binary path with the one from step 1 and `CPANEL_USER` with your
username.

> **If one-minute crons are not allowed** on your plan, use the shortest allowed
> interval (e.g. `*/5 * * * *`). Everything still works — campaigns simply send one
> batch per 5 minutes, so raise *Settings → Sending limits → Emails per cron run*
> accordingly (e.g. 15 → 75 to keep the same hourly throughput).

The Laravel scheduler fans out internally to:

| Task | Frequency | Purpose |
|---|---|---|
| heartbeat | every minute | lets the UI detect a stopped cron |
| `campaigns:process` | every minute, no overlap | sends the next safe batch |
| `imports:process` | every minute, no overlap | stages/processes import rows |
| `queue:work --stop-when-empty --max-time=50` | every minute, no overlap | database queue fallback |
| `data:prune` | daily 02:30 | retention policy |
| `auth:clear-resets` | daily 03:00 | expired reset tokens |

## Direct-command alternative

If you prefer to bypass the scheduler (not recommended — you lose the heartbeat and
housekeeping), add instead:

```cron
* * * * * /usr/local/bin/php /home/CPANEL_USER/fxt-crm-app/artisan campaigns:process >> /home/CPANEL_USER/fxt-crm-mailer.log 2>&1
* * * * * /usr/local/bin/php /home/CPANEL_USER/fxt-crm-app/artisan imports:process >> /home/CPANEL_USER/fxt-crm-import.log 2>&1
```

**Do not configure both the scheduler entry and the direct entries at the same time** —
the atomic locks prevent double-sending, but the runs would fight over the lock and log
noise.

## 3. Verify

1. Wait 1–2 minutes.
2. Admin panel → **System Status**: "Cron: ● Active", recent runs listed.
3. `tail -5 ~/fxt-crm-cron.log` should show scheduler output with no PHP errors.

## Monitoring

- **System Status** page shows: heartbeat age, last campaign/import run, last successful
  run, last error with stack trace, pending/failed queue jobs.
- The **Dashboard** shows a prominent amber banner whenever the heartbeat is older than
  10 minutes.
- Optional email alerts: Settings → General → Admin notification email (auto-paused
  campaigns and processor failures).

## Troubleshooting

| Symptom | Fix |
|---|---|
| Log shows `PHP Parse error` / old PHP | Use the versioned `ea-php83` binary in the cron command |
| `Could not open input file: artisan` | Wrong path — must be the **fxt-crm-app** directory, not the document root |
| Log is empty after 5 min | Cron entry not saved, or cron emails disabled — check cPanel Cron Jobs list |
| Heartbeat active but nothing sends | Email provider unconfigured, campaign paused, or daily limit hit — System Status + campaign page explain which |
| `database is locked` (rare) | Two overlapping runs on slow storage — the lock retries next minute; no action needed |
