# 12.3.18 — cPanel / Passenger hosting hardening (1 GB plan)

No schema, data or screen changes. New file: `hosting.js` (Node built-ins only). New: `.env.example`, `docs/CPANEL_PASSENGER.md`,
`tests/hosting.test.js`.

## Start-up and availability
- **`.env` is now read** (`hosting.loadEnvFile`). Earlier releases documented `.env` but never loaded it; Passenger does not either.
- **Listen first, database second.** The port opens at once; schema checks / self-heal run in the background with back-off retry.
  Passenger's start-up time limit can no longer kill the app on a slow MySQL, and a database outage no longer crash-loops it.
- Until the database is ready, dynamic routes return `503` + `Retry-After` + `X-App-Starting`; the browser retries by itself (`api()` in `common.js`).
- Missing `SESSION_SECRET` in production shows a readable "Configuration needed" page instead of exiting.
- `GET /healthz` (no database, no session) and `GET /readyz` (database ping) for monitors, cron keep-warm and Passenger.
- Optional `KEEP_WARM_URL` self-ping. Graceful shutdown now closes idle keep-alive sockets and both MySQL pools within 8 s.
- A single stray `uncaughtException` is logged and survived; five in a minute recycles the process.

## Memory (1 GB)
- **Memory watchdog:** recycles when idle above ~55 % of the plan, immediately above ~75 % (`MEM_SOFT_MB` / `MEM_HARD_MB`).
- `exceljs` (≈ large) and `geoip-lite` (loads its whole database) are loaded on first use, not at start-up; `GEOIP_DISABLED=1` switches the latter off.
- JSON body limit 20 MB → 2 MB (`JSON_BODY_LIMIT`). Excel export caps 50 000 → 20 000 rows (`EXPORT_MAX_ROWS`); POS sales export 20 000 sales / 60 000 lines.
- Finished workbooks are no longer copied before sending.

## Concurrency
- Uploads (multipart) are limited to 6 at once (`UPLOAD_CONCURRENCY`); Excel exports/imports to 1 at once (`HEAVY_CONCURRENCY`); others wait up to 30–60 s, then get a retryable 503.
- **Static files are served before sessions.** A page load used to cost ~15 session queries; public images, the manifest and PWA icons skip the session store too.
- **Session `touch` throttled** to one UPDATE per session per 5 minutes (it was one per request). Session store retries once on a dropped MySQL connection.
- MySQL: idle connections returned after 60 s (`DB_MAX_IDLE`=3 kept), prepared-statement cache capped (`DB_MAX_PREPARED`=500; mysql2's default of 16 000 per connection can exhaust the server-wide limit), reads and transaction start retried once on a stale connection.

## Proxy / timeouts / uploads
- Keep-alive 65 s (headers 66 s), request timeout explicit (300 s). `trust proxy` configurable (`TRUST_PROXY`).
- Session cookie `Secure` is now `auto` in production (a hard `true` makes express-session drop the cookie when the proxy does not forward the protocol → login loop); `ASSUME_HTTPS=1` / `COOKIE_SECURE` override.
- Static text is compressed once (brotli + gzip, cached in memory); `?v=` assets cached for a year, HTML / `sw.js` always revalidated; JSON compression level 6 → 4.
- File saves are atomic (temp file + rename); replacing a file no longer lists the whole directory (blocking `readdirSync`); stale temp files are cleaned at start.
- Client: reads retry on network failure / 502 / 503 / 504; requests the server says it did not process (starting / busy) retry for ≈ 20 s; a POST that may have been applied is never auto-retried.

## Housekeeping
- Service-worker cache v50 → v51; `?v=` bumped for `common.js`.
