# CLAUDE.local.md

Machine- and user-specific rules for this checkout. Gitignored (`.gitignore:26`) — never
shared with the team, and never merge any of this into the project `CLAUDE.md`.

## Branching — mandatory

- **All work goes on `dev-wk`.** Every change made on this machine belongs on that branch.
- **NEVER push to `master`.** `dev-wk` is the only branch that may be pushed.
- **NEVER commit on `master`.**
- Always push with an explicit refspec — `git push origin dev-wk` — never a bare `git push`,
  which can push `master` under a `matching` push.default.
- Merge **`origin/master`** into `dev-wk` to take upstream changes. Do not check out or fast-forward
  local `master` as a step in that flow; pull upstream via `git fetch origin` + `git merge origin/master`.
- Never force-push without an explicit request in that session.

## Applying changes to the live site — this box serves the working tree DIRECTLY

Apache serves `/var/www/html/peta` as-is, so **PHP changes are live the moment they hit
disk** — but three things do NOT auto-apply, and each has caused a live incident:

1. **Pending migrations** → 500s ("table/column not found"). Check after EVERY merge.
2. **Frontend changes** → the site keeps serving the old `public/build` bundle until a
   build lands. **Build ONLY with `bash scripts/live-build.sh`** — it builds into
   `public/build.new` and swaps with a rename, so the site is never down and a failed
   build changes nothing. **Never** run a bare `npm run build` / `npx vite build` (Vite
   empties `public/build` first → every page 500s for the whole build) and **never**
   wrap a build in `php artisan down` / `up` (that just makes it a 503 — 19 outages on
   2026-08-30 from one session doing exactly that after every small edit). The script
   also carries the memory guard: the box **was** 4GB, where a >1GB build OOM-killed it
   into a 522 + hard-reboot incident (`docs/petav3-dev-incident-summary.md`); it has since
   been resized to **16GB / 4 vCPU** (verified 2026-08-18), but the script still refuses
   below 1200MB `MemAvailable` rather than assuming headroom.
   A 2G `/swapfile` (in fstab) softens overruns.
3. **Queued-job code** → Horizon workers hold old code until `php artisan horizon:terminate`.

**Use `/live-update` (`.claude/commands/live-update.md`) after developing or merging** — it
runs exactly these steps with the guards. `/sync` is the heavier commit → push → deploy loop.

## Deploying

Use `/sync` (`.claude/commands/sync.md`) for the full commit → merge → push → deploy loop.

The deploy itself is `sudo bash scripts/deploy-update.sh --force-build`. Notes that are true
for **this box specifically**:

- The script **aborts on an unclean working tree**, and does `git reset --hard origin/<branch>` —
  so commit and push before deploying or local commits are destroyed.
- Run it in the background; it exceeds a foreground timeout (composer + npm ci + asset build + migrations).
- `--force-build` restarts `baileys-wa-bridge`, dropping the live WhatsApp socket. Omit it if that matters.
- Success means: exit 0, `Deploy complete` **and `Maintenance mode OFF`** in the log, healthy
  service-check table, and HTTP 200 from the live URL. The site serves 503 until maintenance lifts.

### Steps that silently skip on this box — verify, never assume

- **`mysqldump` is NOT installed, so the pre-migration DB backup never runs.** The script guards it
  behind `command -v mysqldump` and skips without an error. A 96-migration deploy ran here with no
  backup on 2026-07-20. Check that `storage/app/deploy/backups/` actually gained a file before
  saying a backup exists. Fix with `sudo apt install mysql-client`.
- `petav3-inertia-ssr.service` does not exist here, so the SSR restart is skipped (harmless — falls
  back to client-side rendering).

## Environment

- Hostname is `petav3-dev`, but `.env` has `APP_ENV=production` and it serves the real domain
  **https://wk.propertylab.com.my**. Treat it as live: deploys cause real downtime.
- App dir `/var/www/html/peta`, owned `ubuntu:www-data`. PHP 8.4. Passwordless sudo available.
- Hardware: **4 vCPU (Intel Xeon @ 2.20GHz) / 16GB RAM / 38GB disk** (~19GB free), plus a 2G
  `/swapfile`. Verified 2026-08-18. *(`/swapfile` is listed TWICE in `/etc/fstab` — harmless,
  the second mount fails, but worth tidying.)*
- Services: php8.4-fpm, Apache, Redis, Horizon, `petav3-scheduler`, `baileys-wa-bridge`.
  Health: `bash scripts/service-check.sh`.
- **Apache serves PHP via `mod_php` under `mpm_prefork`, NOT via php-fpm** — `php8.4-fpm` is
  active but is not in the request path, so restarting it does nothing for a code change
  (`apache2ctl -M` shows `mpm_prefork_module` + `php_module`). Consequences: one OS process
  per concurrent request, and `mod_proxy` / `mod_proxy_wstunnel` / `mod_http2` are all
  **not enabled** (the `.load` files exist, so each is one `a2enmod` away). There is no
  reverse proxy in the vhost, so today nothing external can reach a local port.
- The site is behind **Cloudflare in proxied mode** (`cf-ray: …-SIN`); the origin speaks
  HTTP/1.1 only. Origin IP is hidden.
- **MySQL is remote GCP Cloud SQL** (`DB_HOST=34.87.149.195`), not local — every query is a
  network hop. Redis IS local (`127.0.0.1:6379`) and backs queue, cache and session.
- Outbound persistent WebSockets work (verified `101 Switching Protocols`); inbound does not
  — that needs the Apache modules above *and* a GCP VPC firewall rule (not editable from
  inside the VM; there is no ufw/iptables/nft here).

## Repo quirks

- **`yarn.lock` churn is noise.** The build uses `npm ci` / `package-lock.json`; the local yarn is
  v1 via `npx` and re-resolves the lockfile on every install, producing hundreds of phantom
  diff lines. Revert it (`git checkout -- yarn.lock`) rather than committing it — unless
  `package.json` genuinely changed.
- `.claude/` is gitignored, so local commands and settings are never versioned or backed up.
- Git identity is set repo-locally to `Wai Kit <waikit@propertylab.tech>`.
