# Deploying the CMS-controller server to cPanel (Passenger)

The server is deployed **on its own** (the `web/` control panel is not deployed to
cPanel — run that locally and point it at the deployed API). Target subdomain:
`cmscontroll.shiprexnow.com`.

## cPanel → Setup Node.js App

| Field | Value |
|---|---|
| **Node.js version** | **22.x** (22.5+ required — autopilot v2 uses the built-in `node:sqlite`) |
| **Application mode** | Production |
| **Application root** | the `server` folder of the pulled repo, e.g. `repos/cms_ai_controll/server` |
| **Application URL** | `cmscontroll.shiprexnow.com` |
| **Application startup file** | `app.js` |

> Application root **must** be the `server` directory (not the repo root): that's
> where `package.json`, `package-lock.json` and `node_modules` live. `app.js` is the
> Passenger entry point — it loads `.env`, then boots `src/index.js`.

## Steps

1. **Git pull** the repo onto the host (Bitbucket → cPanel Git Version Control, or
   manual clone/pull).
2. In *Setup Node.js App*, create the app with the settings above.
3. Click **Run NPM Install** (installs from `server/package.json`).
4. **Provide secrets** — either:
   - create `server/.env` on the host (copy `.env.example`, fill real values), or
   - add them under the app's **Environment variables** in the cPanel UI.
   Env vars set in the cPanel UI take precedence over `.env`.
   Required: `SANITY_API_WRITE_TOKEN`, `GEMINI_API_KEY`, `AUTOPILOT_API_KEY` (protects
   the autopilot endpoints), `APP_BASE_URL=https://cmscontroll.shiprexnow.com` (builds
   the approve/reject links). For the review flow: `BREVO_API_KEY`,
   `BREVO_SENDER_EMAIL`, `REVIEW_NOTIFY_EMAILS`. Optional: `SANITY_*`,
   `GEMINI_TEXT_MODEL`, `GEMINI_IMAGE_MODEL`, `SITEMAP_URL`, `MAX_POSTS_PER_DAY`,
   `REVIEW_MODE`, `REVIEW_TOKEN_SECRET`, `BLOG_BASE_URL`. `KB_PATH` is optional —
   it now defaults to the in-repo `Knowledge_Base/kb.md`.
5. **Restart** the app. Verify: `https://cmscontroll.shiprexnow.com/api/health`.

## Autopilot v2: review flow + cron

- With Brevo configured, a run ends as a **Sanity draft** + a review email to
  `REVIEW_NOTIFY_EMAILS` with ✅ Approve / ❌ Reject buttons. Approve publishes the
  post; Reject deletes the draft. Nothing goes live without a click.
- Trigger runs from cPanel **Cron Jobs** (the `x-api-key` header is required):

  ```
  curl -s -X POST https://cmscontroll.shiprexnow.com/api/autopilot/run \
       -H "x-api-key: YOUR_AUTOPILOT_API_KEY" -H "Content-Type: application/json" -d '{}'
  ```

  Schedule it as often as you like (e.g. every 8h) — the built-in
  `MAX_POSTS_PER_DAY` cap (default 2) refuses extra runs with HTTP 429, and a
  cross-process lock refuses overlapping runs with 409.
- **Toggle the guards for development:** `AUTOPILOT_HARDENING=false` opens the
  endpoints (no `x-api-key` needed) and removes the daily cadence cap. Keep it
  `true` in production. The run-lock and stale-run sweep stay on regardless.
- Run state lives in `server/.autopilot/autopilot.db` (SQLite, gitignored) — this is
  also the **angle history** that keeps post styles/audiences varied. Don't delete it
  casually; wiping it resets the diversity memory (harmless, but the first few posts
  may cluster again).

## Notes

- **Do NOT set `PORT`.** Passenger assigns the socket; the app falls back to 8787
  locally and Passenger overrides it in production.
- **`.env` is gitignored** — it is never pulled; create it once on the host (or use
  UI env vars).
- **Point the local control panel at the deployed API:** in `web/vite.config.js`
  change the `/api` proxy target to `https://cmscontroll.shiprexnow.com`, or just use
  Postman/cron against the URL. CORS is open (`cors()`), so the control panel may call
  it from anywhere.
- **Long autopilot runs vs Passenger idle shutdown (important):** a full run takes
  5–60 min and receives no HTTP traffic while working, so Passenger may spin the app
  process down mid-run on shared hosting. This is why the pipeline **checkpoints to
  disk** (`server/.autopilot/`) after every stage and exposes
  `POST /api/autopilot/runs/:id/resume`. If a run is interrupted after the post was
  published, call `/resume` to finish the enhancement on the existing post. To reduce
  interruptions, raise the app's idle timeout / min-instances if your plan allows, and
  poll `GET /api/autopilot/runs/:id` while a run is active (the traffic keeps it warm).
- **Restart after deploys:** `touch tmp/restart.txt` in the app root (cPanel does this
  via the *Restart* button).
