# Hong Kong fork (`petav3-hk` · syncing from `petav3`)

**Context:** the whole repository, not one module · **Upstream:** `git@github.com:property-lab/petav3.git` (remote `upstream`, push disabled) · **Kit:** [`scripts/brand/`](/scripts/brand/README.md) · **Deploy:** `scripts/deploy-update.sh` · **Written:** 2026-09-03, after the first full catch-up (268 upstream commits, 19 conflicts) — everything below was learned doing it.

This doc exists so that a future session can bring `petav3` into this fork **without re-discovering any of it**. Read it end to end before running the sync.

## What it does
`petav3-hk` is **not a separate product**. It is `petav3` (the shared codebase, where every feature is built) plus a thin **brand layer**: a name (*Starcity Global*), a colour ramp (Starcity red `#ED1B24`, near-black navy), a logo, a `zh_HK` catalogue and a handful of Hong Kong content edits. Upstream owns the product; this repo owns only what [`scripts/brand/brand.conf`](/scripts/brand/brand.conf) says.

Two rules follow from that, and every pain point in this doc came from one of them being broken:

1. **Features go upstream, behind a config flag — never into this fork.** The Hong Kong HSBC valuation work, the `zh_HK` catalogue and the per-hostname landing market all live in `petav3`; this fork just turns them on. A feature written here has to be re-merged by hand forever.
2. **Sync by `merge`, never by `cherry-pick`.** A cherry-pick copies a commit under a new hash, after which `git rev-list --left-right --count HEAD...upstream/master` can no longer say what is missing. With merges it is always the honest answer (as of writing: `8  0` — eight brand commits ahead, nothing behind).

## How it works
### The routine (one command, then build, test, deploy)
```bash
cd /var/www/html/peta                      # the production checkout — see "Where to do it" first
bash scripts/brand/sync-upstream.sh        # fetch upstream, branch sync/upstream-<date>, merge, auto-resolve, re-sweep, report
bash scripts/live-build.sh --force         # or: npx vite build — proves the tree compiles
npx vitest run                             # compare against the baseline in "Known failures"
git commit && git push -u origin sync/upstream-<date>
git push origin sync/upstream-<date>:master   # fast-forward master once satisfied
sudo bash scripts/deploy-update.sh --yes   # DB backup → composer/npm → build swap → [down] migrate → cache → fpm → [up] → Horizon → health
```
`deploy-update.sh` is the ONLY way to put new code on the box: it resets the working tree to `origin/master`, so push first. It keeps the outage to the migrate → cache window (~10–20 s), swaps `public/build` atomically, and auto-rolls back the code (not the DB) on failure. Exit 3 = deployed but `service-check.sh` found a service down.

### What the sync script decides, and what it refuses to decide
There is no "ignore these files" list — git has no such thing that would not also freeze them. Instead `brandize.sh` **re-derives** the brand from upstream's own code after every merge: `bg-blue-600` → `bg-brand-600`, `#2563eb` in a chart → the ramp's 600, `/images/logo/logo.png` → `starcity.png`, `PropertyLab` → `Starcity Global`, and the `@theme` block of `resources/css/app.css` itself. It is idempotent.

For each **conflicted** file, `sync-upstream.sh` rebuilds what the fork's version *would be if the only thing the fork ever did to it was rebrand it* (`brandize(merge-base) == ours`):
- **Matches** → the fork has no real change; upstream's file is taken and re-swept. Silent, listed under *auto-resolved*.
- **Does not match** → the fork genuinely edited it. The script **stops** and names it under *NEEDS A HUMAN*. Resolve by hand, then `git add -A && git commit`.

That second list is also a health check. Around 40 files live there permanently and are expected: ~32 are **logo proportions** (the Starcity mark is wider, so `h-9` became `h-20`, `width="160"` became `100` — a design decision no rule can derive) and ~8 carry **real Hong Kong content** (landing copy, contact details). Anything else that lands there is drift: fold it into `brand.conf` or push it upstream.

### Semantic colour vs brand colour — the one class of bug the sweep CREATES
`brandize.sh` maps every `blue` / `indigo` / `sky` utility onto the brand ramp. That is
right for chrome, and wrong wherever upstream used a blue to mean *"neutral / nothing is
wrong here"* — because this fork's brand is **red**. The status ladder on a session's
Registrations tab was the first case found (2026-09-04): `EventRegistration::STATUSES`
spelled Registered `brand`, so a full roster before the doors opened rendered as ten red
alerts sitting beside a red `No-show`, two opposite meanings in one colour.

The fix is never to re-map the ramp — it is to move that token off the brand family, since
the neutral end of a ladder must not be the brand whatever the brand is. Registered is now
`slate`; Attended stays `emerald`, No-show stays `red`. When you meet another one, ask
whether the colour is *decorating* (leave it — brand is correct) or *carrying meaning*
(move it to `slate` / `emerald` / `amber` / `red`, none of which the sweep touches).

⚠️ Do **not** reach for `sky`, `blue` or `indigo` as the replacement: the next sweep turns
them back into brand red and the bug returns silently. `EventRegistration::STATUSES` and
`RegistrationsTab.vue`'s tile map are fork-edited for this reason and will show up under
*NEEDS A HUMAN* on future syncs — keep the fork's side.

### Resolving what the script stops on — the decisions already made
These were taken on 2026-09-03 and should be kept unless the operator says otherwise:
| File | What happened | Decision |
|---|---|---|
| `resources/lang/zh_HK.json` | Written **twice on the same day** — 1013 keys on this fork, 951 upstream, 112 shared keys translated differently | **The fork's wording wins** (it is the Hong Kong operator's own catalogue). Upstream's extra keys were only the un-rebranded twins of strings the fork already had. Merge with the small PHP union in the git log of `cfa4305f8`; never let a `PropertyLab`-keyed twin survive the sweep, or the key duplicates. |
| `config/app.php` locale aliases | Both sides added alias tables | **Union.** Upstream also routes `zh-TW` → `zh_HK`, which this fork had deliberately not done; taken as upstream wrote it (shared code; a Traditional reader is better served by `zh_HK` than by the Simplified fallback). |
| `resources/js/Components/ProjectDetail/ContactPanel.vue` | Upstream **deleted** it (`ProjectTabsParity.test.js` asserts the file must not exist); the fork's only change was the brand sweep of sample rows | Deletion stands — `git rm`. |
| `resources/css/app.css`, `AppLogo.vue`, `AppShell.vue`, `SiteLayout.vue`, `Home.vue` | Fork side is the red palette + text lockup; sync side was only a sweep of an older version | Take **master's** version, then update `RAMP_*` in `brand.conf` to match `app.css` and re-run `brandize.sh` so charts follow. |

### Where to do it
Do the merge in a **`git worktree`** off the production checkout, not in `/var/www/html/peta` itself — that directory is the live site, and a half-merged tree is served immediately. `git worktree add -b sync/upstream-<date> <scratch> origin/master`, copy `node_modules` in (`package.json` rarely changes), work there, push, then deploy on the box. The script itself does not care which checkout it runs in.

## Pitfalls — each one cost time once
- **The box was on a detached HEAD** with uncommitted Hong Kong work for hours. `git pull` fails, nothing records what is deployed, and the work is one `reset` from gone. Fixed with `git checkout -B master origin/master` (same commit → no file changes). Keep it on `master`; commit WIP before any sync (`git status` must be clean or the script refuses).
- **`.env` edits do nothing until `php artisan config:cache`** — the config is cached — **and Horizon must be restarted** (`sudo systemctl restart horizon`; `horizon:terminate` reported "No processes to terminate" here). In code, read `config(...)`, never `env(...)`: with the cache on, `env()` returns `null`.
- **`resources/lang/*.json` must be swept key AND value.** In Laravel's JSON translations the key *is* the English source string; renaming the brand in a `.vue` without renaming the key silently drops the zh_CN / ms / zh_HK line and shows English. Four keys were already broken this way before the first sync.
- **`PropertyLab` glued to more letters is a class name** (`PropertyLabEvaluationController`, `EvaluatePropertyLabGeneration`). A blind replace fatals the app. `brandize.sh` matches only the standalone word (perl lookarounds) — keep it that way.
- **Lower-case `propertylab` is never renamed.** It is a config key, an AI persona id, a stored column value and a domain (`propertylab_ai`, `app.propertylab.com.my`). Renaming breaks lookups against rows already in the database.
- **Directional utilities are colours too.** The first sweep missed `border-t-indigo-500` / `divide-x-blue-200` / `ring-offset-sky-100` and left single blue edges in 14 files. The regex now covers `border-[xytrbles]`, `divide-[xy]`, `ring-offset`.
- **Navy has its own ramp** (`#2d4368`…`#0b1626` upstream). It was never mapped at first, so the maintenance page, the Road viz palette and several gradients rendered upstream's navy on a graphite site. `NAVY_*` in `brand.conf`.
- **`app.css` is not hand-owned.** It is byte-for-byte `brandize(upstream's app.css)`. If you hand-edit the palette, put the same hex in `brand.conf`, or the next sync hands it back as a conflict — and charts drift a different colour from the page.
- **`index.lock: File exists` mid-loop.** A `git add` inside the auto-resolve loop once collided with another git process; the loop carried on and left one file (`config/ai.php`) with conflict markers *staged*. After any sync: `git grep -l '^<<<<<<< '` and `php -l` on every changed `.php` before committing.
- **`yarn.lock` gets rewritten by the deploy's npm step.** The project uses npm; the lock is a leftover. `git checkout -- yarn.lock` afterwards.
- **New `.env` keys arrive with every sync.** Diff `.env.example` between the old and new commit (not against the whole file — most of it was always missing and has defaults) and append the genuinely new keys with their example defaults; back the file up first (`~/.env.backup-<date>`).
- **A build peaks at ~2.85 GB RSS and has OOM-killed this 7.8 GB box before.** `live-build.sh` refuses under 3500 MB available. Run one heavy thing at a time (build, then tests), `nice -n 19`, `NODE_OPTIONS=--max-old-space-size=3072`.

## Known failures (the baseline)
`npx vitest run` on a correct sync is **not green**. As of upstream `7a8a81310` the same 22 tests fail on pristine `petav3` — verify by running the failing files in a worktree of `upstream/master` before assuming the sync broke them:
- `resources/js/Pages/Manage/Zoom/Evaluation/Index.test.js` (18) and `Pages/Manage/PropertyMatch/admin.test.js` (2) — `usePage().url` is undefined under the test mock (`composables/useActivePath.js:16`).
- `Components/ProjectDetail/AmenitiesPanel.test.js` (1).
- `utils/portalGuide/portalGuide.test.js` (1) — a lesson's `reviewedAt` must post-date every file it renders; fails on any day those screens were edited.

A sync is acceptable when the failure set equals upstream's. A *new* failure is the sync's.

## Starting a third brand
1. Fork `petav3` on GitHub — **not this repo** — so the new brand inherits no Hong Kong decisions.
2. Copy `scripts/brand/` across; edit `brand.conf` only: name, upper-case name, the ten `RAMP_*` and five `NAVY_*` hex, `LOGO_SUBS`, `EXTRA_SUBS`.
3. Put the same hex into the `@theme` block of `resources/css/app.css` (or just run the sweep — it rewrites that block from upstream's values).
4. Drop the logo files into `public/images/logo/` under new names (different filenames never conflict).
5. `bash scripts/brand/brandize.sh` once, whole repo. Build, look, commit.
6. `.env` carries domains, mail and keys; nothing else in the repo should differ from upstream.

## Related files
**Kit** — [`scripts/brand/README.md`](/scripts/brand/README.md) (the rules and why), [`scripts/brand/brand.conf`](/scripts/brand/brand.conf), [`scripts/brand/brandize.sh`](/scripts/brand/brandize.sh), [`scripts/brand/sync-upstream.sh`](/scripts/brand/sync-upstream.sh).
**Deploy** — [`scripts/deploy-update.sh`](/scripts/deploy-update.sh), [`scripts/live-build.sh`](/scripts/live-build.sh), [`scripts/service-check.sh`](/scripts/service-check.sh); runbooks in [`/docs/modules_handbook/production-setup/`](/docs/modules_handbook/production-setup/horizon.md).
**Brand-owned by hand** — `resources/css/app.css` is swept, but `AppShell.vue` / `SiteLayout.vue` / `Home.vue` carry the text lockup and red hero and will always stop the script; `public/images/logo/red-text-non-bg.png` (transparent, for light grounds), `starcity-red-text.png` (white ground — email, PDF, dark surfaces) and `starcity-mark.png` (the square mark, for favicon/avatar sizes). The older `starcity.png` / `starcity-red-bg.png` / `starcity-ai.png` were retired 2026-09-06.
**Hong Kong features (upstream-owned, enabled here)** — `resources/lang/zh_HK.json`, `config/app.php` locale aliases, `app/Support/LocalePreset.php`, `market_sites.default_country_id` (`app/Http/Middleware/SetPublicSite.php`, `src/Common/MarketSite.php`), the `feat(hk)` HSBC valuation ingestion.
**History** — `ba397a99a` first catch-up (214 commits), `ca1333737` / `7c8d37433` the kit, `fe910757b` the committed Hong Kong WIP, `cc68ad8f6` + `cfa4305f8` the second pass; each message records what was decided and why.
