# Deploying to cPanel (FTP only)

Production URL: **https://support.techronltd.co.ke**

This host has **no SSH**. Build everything on your PC, upload via **FTP** or **cPanel File Manager**, and configure MySQL + cron through the cPanel web UI.

## PHP version requirement (read first)

The deployed `vendor/` tree (Laravel 12 + Symfony 7) requires **PHP >= 8.2.0**. PHP 8.1 or lower will fail with:

```text
Composer detected issues in your platform: Your Composer dependencies require a PHP version ">= 8.2.0".
```

**Fix on cPanel (per domain — does not change the whole account):**

1. **MultiPHP Manager** → select `support.techronltd.co.ke` → set **PHP 8.2**.
2. **Select PHP Version** → PHP **8.2** → enable: `pdo`, `pdo_mysql`, `mbstring`, `fileinfo`, `zip`, `dom`, `openssl`, `curl`.
3. File Manager → `backend/bootstrap/cache/` → delete `packages.php` and `services.php` (keep `.gitignore`).
4. Open https://support.techronltd.co.ke/ping.php — expect `php_version: ok` and `laravel_autoload: ok`.
5. Cron must use **`ea-php82`** (CLI PHP must match web PHP and include `pdo_mysql`).

Diagnostic: https://support.techronltd.co.ke/ping.php — remove `ping.php` after the site is healthy.

## Build output locations

| What | Where |
|------|--------|
| React SPA (Vite build) | `backend/public/index.html` + `backend/public/assets/` |
| Deploy zip | `dist/support-deploy.zip` (project root) |
| **Not used** | `frontend/dist` — Vite is configured to write directly into `backend/public` |

## Server layout

Upload the prepared Laravel app to:

```text
/home/techronl/support.techronltd.co.ke/backend/
```

Set the subdomain **document root** to:

```text
/home/techronl/support.techronltd.co.ke/backend/public
```

**If cPanel will not let you point at `backend/public`** (document root is the subdomain folder and API hits `/home/.../support.techronltd.co.ke/index.php`), extract the zip so you have:

```text
/home/techronl/support.techronltd.co.ke/
  index.php          <- from zip root (cpanel-public/)
  .htaccess          <- from zip root (cpanel-public/)
  backend/           <- Laravel app + public/assets/
```

Do **not** upload: `frontend/node_modules/`, `.git/`, `backend/tests/`, or local-only `backend/.env.production`.

## Quick start (populated database — no import)

Use this when the online database **already has data** and you only need to deploy code updates.

1. **Create production env on your PC**

   ```powershell
   cd backend
   copy .env.production.example .env.production
   ```

   Fill in `APP_KEY`, `DB_PASSWORD`, `MAIL_*`, `IMAP_*`, and `INFOBIP_*` in `.env.production`.

   Generate `APP_KEY` once:

   ```powershell
   copy .env.production .env
   php artisan key:generate
   ```

   Copy the generated `APP_KEY` back into `.env.production`.

2. **Apply pending migrations only** (safe — does not wipe data)

   cPanel → **Remote MySQL** → whitelist your PC's IP. In `.env.production`, temporarily set `DB_HOST` to the remote MySQL hostname from cPanel (not `localhost`).

   ```powershell
   cd backend
   copy .env.production .env
   php artisan migrate:status
   php artisan migrate --force
   ```

   **Never run** `migrate:fresh`, `legacy:import --fresh`, or import SQL dumps over a populated production database.

3. **Build and zip**

   ```powershell
   cd ..
   .\scripts\build-deploy.ps1
   ```

4. **Upload** `dist/support-deploy.zip` via FTP and extract to `/home/techronl/support.techronltd.co.ke/backend/`.

   On the server `.env`, ensure `DB_HOST=localhost`.

5. **Add cron** (see below) and run the [post-deploy verification](#post-deploy-verification) checklist.

## One-click local build (Windows)

From the repo root:

```powershell
.\scripts\build-deploy.ps1
```

This will:

1. Build the React SPA into `backend/public/` (not `frontend/dist`)
2. Run `composer install --no-dev` (does **not** config-cache — safe for cPanel FTP)
3. Create `dist/support-deploy.zip` ready for FTP upload

Skip the zip step only:

```powershell
.\scripts\build-deploy.ps1 -SkipZip
```

### First-time setup on your PC

1. Copy `backend/.env.production.example` to `backend/.env.production` and fill in production DB, mail, IMAP, and Infobip credentials.
2. Run `php artisan key:generate` once with `.env.production` copied to `.env`; copy the generated `APP_KEY` back into `.env.production`.
3. Run the build script (uses `-EnvFile .env.production` by default).

## Manual build steps

```powershell
cd frontend
npm ci
npm run build

cd ..\backend
copy .env.production .env
composer install --no-dev --optimize-autoloader
php artisan storage:link
```

Do **not** run `config:cache`, `route:cache`, or `view:cache` on Windows before FTP upload — cached files break on Linux cPanel.

Zip the `backend/` folder (excluding `tests/`) and upload via FTP, or use `dist/support-deploy.zip` from the build script.

## Database (no SSH on server)

Production database: **`techronl_supportnew`** (user: `techronl_supportnew`).

### Option A — migrate from your PC (recommended for updates)

1. cPanel → **Remote MySQL** → allow your home/office IP.
2. Put production DB credentials in `backend/.env.production`.
3. Set `DB_HOST` to the **remote MySQL hostname** from cPanel (for PC access only).
4. On your PC:

   ```powershell
   cd backend
   copy .env.production .env
   php artisan migrate:status
   php artisan migrate --force
   ```

5. Run the build script and upload via FTP.
6. On the server `.env`, use `DB_HOST=localhost`.

### Option B — populated DB, code-only deploy

If the online database is already populated and current:

- **Do not import** SQL dumps or run `legacy:import --fresh`.
- Run `migrate:status` / `migrate --force` only when new migrations exist in the repo.

### Option C — first-time cutover from legacy (empty production DB only)

```powershell
cd backend
php artisan migrate --force
php artisan legacy:import --fresh --sql=../techronl_ticket.sql
```

Use only when production is **empty** and you are migrating from the old ticket system.

### Option D — phpMyAdmin

1. Locally run migrations, then export schema (`php artisan schema:dump` or mysqldump).
2. cPanel → **phpMyAdmin** → import into the production database.

## FTP upload checklist

| Upload | Notes |
|--------|--------|
| `backend/` (full tree with `vendor/`) | Main application |
| `backend/.env` | Production secrets — upload separately; merge keys if server `.env` already exists |
| `backend/public/` | SPA `index.html`, `/assets/`, `index.php`, `.htaccess` |
| `backend/storage/` | Writable dirs; **preserve** `storage/app/tickets/` on updates |

Do **not** overwrite on routine updates:

- `backend/.env` (unless merging new keys)
- `backend/storage/app/tickets/` (ticket attachments)

Set folder permissions via File Manager: **755** on `storage/` and `bootstrap/cache/` (775 if the host requires it).

### Storage symlink

Run `php artisan storage:link` locally before upload. If FTP does not preserve symlinks, use cPanel File Manager → **Create Symbolic Link**: `public/storage` → `../storage/app/public`.

## Production `.env` values (server)

Use `backend/.env.production.example` as the template. On the **server**, at minimum:

```env
APP_ENV=production
APP_DEBUG=false
APP_URL=https://support.techronltd.co.ke
FRONTEND_URL=https://support.techronltd.co.ke
APP_TIMEZONE=Africa/Nairobi

DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=techronl_supportnew
DB_USERNAME=techronl_supportnew
DB_PASSWORD=<your password>

SESSION_DOMAIN=support.techronltd.co.ke
SANCTUM_STATEFUL_DOMAINS=support.techronltd.co.ke
QUEUE_CONNECTION=database
LOG_LEVEL=warning
```

Fill in `APP_KEY`, `MAIL_*`, `IMAP_*`, and `INFOBIP_*` with production credentials.

## Cron jobs (cPanel → Cron Jobs)

**Prerequisite:** PHP **8.2+** and `/ping.php` shows `laravel_autoload: ok`. No cron works if Composer platform check fails.

**Paths (confirm in File Manager):**

```text
PHP:     /usr/local/bin/ea-php82
Artisan: /home/techronl/support.techronltd.co.ke/backend/artisan
```

Laravel’s scheduler in `routes/console.php` runs email polling, WhatsApp retry, SLA escalation, manager digest, and the queue worker. You can use **one** cron (`schedule:run`) or **five** separate artisan crons — not both.

### Step 1 — Set the cron notification email

At the top of **cPanel → Cron Jobs**, find **Cron Email** (or “Email”):

1. Enter the address where you want cron output sent (e.g. your work email).
2. Click **Update Email**.

cPanel emails you the **stdout/stderr** of each cron run. If the command prints nothing and redirects output to `/dev/null`, you will **not** get emails.

### Step 2 — Find your PHP binary and home path

Your host uses the `ea-phpXX` style (see the red example on the Cron Jobs page):

```text
/usr/local/bin/ea-php99 /home/techronL/domain_path/path/to/cron/script
```

Do this before adding the cron:

1. **cPanel → MultiPHP Manager** → `support.techronltd.co.ke` must be **PHP 8.2** → use `ea-php82` in cron commands.
2. **cPanel → File Manager** → confirm `backend/artisan` path (often `/home/techronl/support.techronltd.co.ke/backend`).

Replace the `/home/...` path below if yours differs.

### Step 3 — Test cron with email (do this first)

Add a **temporary** cron job to confirm cPanel can run PHP and email you.

**Form fields (cPanel “Add New Cron Job”):**

| Field | Value |
|-------|--------|
| Common Settings | **Once Per Minute (* * * * *)** |
| Minute | `*` |
| Hour | `*` |
| Day | `*` |
| Month | `*` |
| Weekday | `*` |
| Command | See below |

**Command** (copy and edit paths):

```text
/usr/local/bin/ea-php82 /home/techronl/support.techronltd.co.ke/backend/artisan cron:ping
```

Important:

- **Do not** add `>> /dev/null 2>&1` during testing — that suppresses email.
- Wait **1–2 minutes**, then check the inbox set in Step 1.

**Expected email body** (one line per minute):

```text
TechRon Support cron OK — 2026-08-13 12:45:01 (Africa/Nairobi)
```

Also check via FTP: `backend/storage/logs/laravel.log` should contain the same `TechRon Support cron OK` lines.

If you get **no email**:

| Problem | Fix |
|---------|-----|
| Wrong PHP path | Try `ea-php82` or check MultiPHP Manager |
| Wrong artisan path | Verify path in File Manager |
| Command failed | Email may contain PHP error — read it carefully |
| Spam folder | Search for mail from your hosting account |

When the test email arrives, **delete** the temporary `cron:ping` cron job.

### Step 4 — Production cron

#### Option A — One cron (recommended)

Add **one** permanent cron job (`* * * * *`):

```text
/usr/local/bin/ea-php82 /home/techronl/support.techronltd.co.ke/backend/artisan schedule:run >> /dev/null 2>&1
```

**Test mode** (emailed output — omit `/dev/null`, add `-v`):

```text
/usr/local/bin/ea-php82 /home/techronl/support.techronltd.co.ke/backend/artisan schedule:run -v
```

Do **not** combine `-v` with `>> /dev/null` — that hides verbose output.

If cron email shows `could not find driver`, CLI PHP is missing `pdo_mysql` — use **Option C**.

#### Option B — Individual cron jobs (each task separate)

Use if you want each job listed separately in cPanel. **Do not also run Option A.**

Prefix:

```text
/usr/local/bin/ea-php82 /home/techronl/support.techronltd.co.ke/backend/artisan
```

Append `>> /dev/null 2>&1` for silent production.

| Job | cPanel schedule | Artisan command | Purpose |
|-----|-----------------|-----------------|---------|
| Email poll | `* * * * *` | `emails:poll` | Poll Zoho IMAP (requires **Email polling ON** in Admin Settings + `IMAP_*` in `.env`) |
| WhatsApp retry | `* * * * *` | `whatsapp:retry-inbox` | Retry failed inbound WhatsApp webhook processing |
| Queue worker | `* * * * *` | `queue:work --max-time=55 --sleep=1 --tries=3` | Drain WhatsApp jobs (`queue.pending` in `/api/health`) |
| SLA escalation | `*/5 * * * *` | `tickets:escalate-overdue` | Escalate SLA-overdue tickets |
| Manager digest | `30 7 * * *` | `reports:manager-digest` | Daily digest at **07:30** (`APP_TIMEZONE=Africa/Nairobi`) |

**Example rows:**

```text
* * * * * /usr/local/bin/ea-php82 /home/techronl/support.techronltd.co.ke/backend/artisan emails:poll >> /dev/null 2>&1
* * * * * /usr/local/bin/ea-php82 /home/techronl/support.techronltd.co.ke/backend/artisan whatsapp:retry-inbox >> /dev/null 2>&1
* * * * * /usr/local/bin/ea-php82 /home/techronl/support.techronltd.co.ke/backend/artisan queue:work --max-time=55 --sleep=1 --tries=3 >> /dev/null 2>&1
*/5 * * * * /usr/local/bin/ea-php82 /home/techronl/support.techronltd.co.ke/backend/artisan tickets:escalate-overdue >> /dev/null 2>&1
30 7 * * * /usr/local/bin/ea-php82 /home/techronl/support.techronltd.co.ke/backend/artisan reports:manager-digest >> /dev/null 2>&1
```

**Manager digest time:** cPanel cron uses the **server clock**. If the server is UTC, adjust the hour (Nairobi 07:30 EAT = 04:30 UTC → use `30 4 * * *`).

#### Option C — HTTP trigger (when CLI lacks `pdo_mysql`)

Web PHP works ( `/api/health` connects to DB) but CLI `artisan` often fails on shared hosts.

1. Upload `backend/public/schedule-run.php`
2. In `backend/.env`, set a long random secret (generate on server only — never commit):

   ```env
   CRON_SECRET=
   ```

3. cPanel cron (`* * * * *`):

   ```text
   curl -fsS "https://support.techronltd.co.ke/schedule-run.php?token=YOUR_CRON_SECRET" >> /dev/null 2>&1
   ```

4. Test in browser (once): `https://support.techronltd.co.ke/schedule-run.php?token=YOUR_CRON_SECRET` — should print `schedule:run exit 0`

**Test mode:** omit `>> /dev/null 2>&1` to receive cron email each minute.

Use Option C only when Option A fails with `could not find driver`. On hosts where CLI PHP matches web PHP, Option A or B is preferred (no secret in cron URL).

### Step 5 — Confirm the scheduler is running

After 2–3 minutes with the production cron in place:

1. Download `backend/storage/logs/laravel.log` via FTP.
2. Look for log lines from scheduled commands, for example:
   - IMAP poll messages from `emails:poll`
   - Queue worker activity
   - `TechRon Support cron OK` only if you still have `cron:ping` scheduled

Optional one-off verbose test (run manually is not possible without SSH; use log instead):

- Temporarily change the production command to (for **2 minutes only**):

```text
/usr/local/bin/ea-php82 /home/techronl/support.techronltd.co.ke/backend/artisan schedule:run -v
```

You should receive an email listing which scheduled tasks ran. Then restore the production command with `>> /dev/null 2>&1`.

### Alternative: PHP-type cron in cPanel

If cPanel offers a **PHP** cron type instead of a shell command:

1. Choose PHP **8.2** for the domain.
2. Set the command to:

```text
/home/techronl/support.techronltd.co.ke/backend/artisan schedule:run
```

Add `>> /dev/null 2>&1` only after you have confirmed it works.

### What the scheduler runs

| Command | Schedule | Purpose |
|---------|----------|---------|
| `schedule:run` | Every minute (cPanel cron) | Laravel scheduler entry point |
| `emails:poll` | Every minute | Poll Zoho IMAP; create/reply tickets from email |
| `whatsapp:retry-inbox` | Every minute | Retry failed WhatsApp webhook processing |
| `tickets:escalate-overdue` | Every 5 minutes | Escalate SLA-overdue tickets; notify staff |
| `reports:manager-digest` | Daily 07:30 (Africa/Nairobi) | Manager queue digest (if enabled in Admin Settings) |
| `queue:work --max-time=55 --sleep=1 --tries=3` | Every minute | Drain WhatsApp notification jobs (keeps polling for the full 55s instead of exiting the instant the queue is empty, so a job dispatched right after a tick doesn't wait for the next one) |

### Not cron jobs

| Action | When |
|--------|------|
| Ticket emails | Sent immediately on close/assign/reply (no queue) |
| Infobip inbound webhook | Real-time HTTP POST to `/api/webhooks/infobip/whatsapp` |
| Migrations / config cache | Run locally before FTP upload |
| `legacy:import` | Run locally during cutover only (empty DB) |
| `cron:ping` | Temporary cron test only — remove after verify |
| `whatsapp:test` | Manual admin/CLI test only |

## Infobip webhook URL

Set in Infobip dashboard:

```text
https://support.techronltd.co.ke/api/webhooks/infobip/whatsapp
```

## Post-deploy verification

- [ ] https://support.techronltd.co.ke/ping.php shows `php_version: ok` (PHP 8.2+) and `laravel_autoload: ok`
- [ ] https://support.techronltd.co.ke loads the login page (not a directory listing)
- [ ] Hard refresh on `/admin/companies` does not 404 (SPA + `.htaccess` working)
- [ ] https://support.techronltd.co.ke/api/health returns `"status":"ok"` and `"database":"connected"`
- [ ] https://support.techronltd.co.ke/up returns OK (Laravel health)
- [ ] Login stays active after redirect (no immediate bounce to `/login`; `/api/notifications` returns 200)
- [ ] Registration: company search works; unlisted companies use **Other**
- [ ] Companies page shows total count and scrollable table
- [ ] Close a ticket → staff “closed” emails arrive
- [ ] Cron test email received from `cron:ping` (then remove test cron)
- [ ] Production cron active (Option A **or** Option B — not both)
- [ ] https://support.techronltd.co.ke/api/health → `queue.pending` drops over time
- [ ] `storage/logs/laravel.log` shows scheduler activity after production cron runs
- [ ] Send test WhatsApp from Admin Settings → no rows stuck in `jobs` table
- [ ] Infobip webhook URL configured

### Common failures

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `Composer … require PHP >= 8.2.0` / `laravel_autoload` fail on ping | Domain set to PHP 8.1 or lower | MultiPHP Manager → **PHP 8.2** for this domain; delete `bootstrap/cache/packages.php` + `services.php` |
| Blank page / old UI | Stale `backend/public/assets` | Re-run `npm run build`, re-upload `public/` |
| 500 on `/` but `/index.html` works | LiteSpeed/Apache runs `index.php` before `index.html` | Ensure `backend/public/.htaccess` has `DirectoryIndex index.html index.php` and re-upload |
| 500 on `/api/*` (login, health) | Laravel not booting on server | Open `/ping.php` or see **Fix API 500** below |
| Login fails: `localhost:8080/api/... ERR_CONNECTION_REFUSED` | Production build had `VITE_API_URL=http://localhost:8080` in `frontend/.env` | Ensure `frontend/.env.production` has `VITE_API_URL=` empty, rebuild, re-upload `backend/public/assets/` |
| DB connection error | Wrong `DB_*` on server | `DB_HOST=localhost` on server; correct db/user |
| 404 on `/admin/...` refresh | Document root not `backend/public` | Fix cPanel document root |
| Login works then immediately logs out (401 on `/api/notifications`, `/api/admin/dashboard`) | Apache/LiteSpeed strips `Authorization` before PHP, or document root is the subdomain folder (not `backend/public`) | Upload **`index.php` + `.htaccess` from zip root** (from `cpanel-public/`) to `/home/<user>/support.techronltd.co.ke/`; re-upload `backend/public/.htaccess`, `backend/bootstrap/authorization.php`, `backend/bootstrap/app.php`; rebuild frontend for `X-Authorization` fallback; test `curl -H "Authorization: Bearer test" https://support.techronltd.co.ke/ping.php` shows `authorization_header: ok` |
| Emails/WhatsApp silent | Cron not running | Add/fix cron entry |

### Fix API 500 (login, health, all `/api/*`)

The login page can load (static SPA) while **every** API call returns 500. Check in order:

0. **Quick diagnostic (no Laravel)**  
   Open https://support.techronltd.co.ke/ping.php — JSON lists missing `vendor/`, empty `APP_KEY`, stale `bootstrap/cache/*.php`, or missing PHP extensions. Delete `ping.php` after the site is healthy.

1. **Read the error log (most important)**  
   cPanel → File Manager → `backend/storage/logs/laravel.log`  
   Download and open the **last 30 lines**. The exact exception tells you what to fix.

2. **Delete bootstrap cache (Windows-built cache breaks Linux)**  
   File Manager → `backend/bootstrap/cache/`  
   Delete every file **except** `.gitignore` (remove `config.php`, `routes-v7.php`, `packages.php`, `services.php` if present).

3. **Verify `backend/.env` on the server**  
   Must exist in `backend/` (not only on your PC). Required:

   ```env
   APP_KEY=base64:...   # must not be empty
   APP_ENV=production
   APP_DEBUG=false
   DB_HOST=localhost
   DB_DATABASE=techronl_supportnew
   DB_USERNAME=techronl_supportnew
   DB_PASSWORD=...
   ```

   Generate `APP_KEY` locally if missing:

   ```powershell
   cd backend
   copy .env.production .env
   php artisan key:generate
   ```

   Copy the key into server `backend/.env` via FTP.

4. **Verify `backend/vendor/` was uploaded**  
   The folder must exist and contain many packages (e.g. `vendor/laravel/`).  
   If missing or incomplete, re-run `composer install --no-dev` locally and re-upload the whole `vendor/` folder (large — use zip).

5. **Permissions**  
   `storage/` and `bootstrap/cache/` → **755** or **775**.

6. **Test**  
   Open https://support.techronltd.co.ke/api/health — should return JSON like:

   ```json
   {"status":"ok","database":"connected",...}
   ```

   Then try login again.

**Build note:** `build-deploy.ps1` no longer runs `config:cache` on Windows (that caused path errors on cPanel). Do not manually upload `bootstrap/cache/*.php` from your PC.

## Ongoing updates via FTP

1. Run `.\scripts\build-deploy.ps1` locally.
2. Upload changed files, or extract a new zip over `backend/` **without** overwriting:
   - `backend/.env`
   - `backend/storage/app/tickets/` (attachments)
3. If new migrations exist, run `php artisan migrate --force` from your PC (remote MySQL) before or after upload.
4. Cron keeps running — no restart needed unless queue configuration changes.
